Click here to Skip to main content
Click here to Skip to main content

Windows 8 : Fun with sensors

, 29 Jan 2013
Rate this:
Please Sign up or sign in to vote.
Simple Windows 8 app that tries to have some fun with sensors
Demo code : SimpleWin8App.zip

Introduction

It has been a while since I have written an article, and I thought it was about time that I wrote one. This one is a very small one (which is somewhat unusual for me). It is a very very simple one on using some of the Windows 8 sensors. I am lucky enough to own a Windows 8 laptop that has the full compliment of sensors. So what does it do I hear you ask? Sensors are all about stuff happening, so I thought it should be some code that is quite reactive to the sensors that I chose to use. With that in mind what I thought might be cool would be to host Google Earth and have that either displaying the Earth or the Sky depending on the value of the LightSensor. When the hosted Google Earth is flipped between Earth/Stars the skin of the application is also swapped. Hopefully by swapping these 2 visual elements in reaction to the LightSensor you can see how the LightSensor code is working.

I also thought it might be fun to have the Earth/Sky change Latitude/Longitude as you tilt the laptop which causes the inbuilt Gyrometer to push out new readings.

In a nutshell that is all this article does, its quite simple (which is a good thing, as it keeps the code short, and the article short too). One area where you may have to fiddle is with certain magic values, which I have tested with my laptop and my own use of the attached demo code, which has been at home and on the move. I will mention these again as we go through the article and the demo code inner workings. Just bear in mind that you WILL more than likely need to play with some values before the attached code works as it is meant to.

Pre-requisites

You will need to have the following items in order to run this application

  • Google Earth plugin
  • A persistent internet connection
  • Windows 8
  • A Laptop which has at least the following sensors
    • LightSensor
    • Gyrometer

Some Screen Shots

Here are some simple screen shots which demonstrate what the app should look like when it's running correctly

Earth Shown : LightSensor thinks it is Light (Governed by magic setting)

This is what the demo app should look like if the LightSensor current reading is above GlobalSettings.ValueToBeConsideredDark

Sky Shown : LightSensor thinks it is Dark (Governed by magic setting)

This is what the demo app should look like if the LightSensor current reading is below GlobalSettings.ValueToBeConsideredDark

The Demo App

The following subsections will discuss the demo app code and its inner workings

How To Use The Demo App

Now that you know what this dead simple app is trying to do, let's talk about how to run it shall we. Truth is to run it, all you need to do is load up VS2012 on a Windows 8 machine and run it, and maybe fiddle with some of the GlobalSettings

However I have found to get the best results I had to kind of do the following

  • Have the laptop/tablet on a flat surface such as a table
  • Tilt the laptop/tablet from front to back slowly (if testing latitude/longitude)
  • Tilt the laptop/tablet from back to front slowly (if testing latitude/longitude)
  • Have the laptop/tablet on a flat surface such as a table, and turn on/off the main room light (if testing flipping between Google Earth/Sky)

It can be seen that you you should simply tilt the laptop up and down (I have found the best way is to lift keyboard up and down). Pretty self explanatory I think

Magic Settings

When I decided to finally play around with Windows 8/Sensors I was pleasntly suprised by how easy it was, but was also quite dismayed that I would get wildly different reading readings from the Sensors depending on what room of my house I was in. For example the LightSensor would be between -2 to 8 in one room of my house, and would be between -10 and 20 in another room of my house. So to combat that I have had to cheat a little and introduce several system settings, which you WILL have to play around with, as these are currently set to what I deemed correct, and which produced the best results for me, which obviously may produce a bad result for you. So play around with them, they are all available within a single settings file called "GlobalSettings" which looks like this

public class GlobalSettings
{
    /// <summary>
    /// Below this value is considered to be dark, so will show Sky
    /// </summary>
    public static readonly float ValueToBeConsideredDark = 8;

    /// <summary>
    /// Minimum amount of change between current and last reading(s) for Gyrometer, so we do not react to every event we see
    /// on those where a minimum change has occurred
    /// </summary>
    public static readonly double MinLatitudeChange = 0.3;
    public static readonly double MinLongitudeChange = 0.3;
}

Google Earth Web Code

As previously stated the idea behind this demo app was to swap in out a hosted Google Earth plugin, to show either Earth/Stars depending on the LightSensor current value. To that end there is obviously some web content that has to be hosted. The following code is the entire HTML file that is loaded into a standard WPF WebBrowser control

<!DOCTYPE html>
<html>
<head>
    <title>Simple Windows 8 Sensor Fun</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />

    <!-- Change the key below to your own Maps API key -->
    <!--<script type="text/javascript" 
       src="http://www.google.com/jsapi?hl=en&key=ABQIAAAAwbkbZLyhsmTCWXbTcjbgbRSzHs7K5SvaUdm8ua-Xxy_-2dYwMxQMhnagaawTo7L1FE1-amhuQxIlXw"></script>-->
    <script type="text/javascript" src="http://www.google.com/jsapi"></script>
    <script type="text/javascript">
        var ge;
        var placemark;
        var currentLatitude = 37;
        var currentLongitude = -122;
        var currentAltitude = 8000000;
        var isShowingSky = false;
        var skyFlySpeed = 0.2;
        var earthFlySpeed = 5;


        google.load("earth", "1");

        function init() {
            google.earth.createInstance('map3d', initCB, failureCB);
        }

        function initCB(instance) {
            ge = instance;
            setCommonOptions();

            ge.getWindow().setVisibility(true);

            // add a navigation control
            ge.getNavigationControl().setVisibility(ge.VISIBILITY_HIDE);

            // add some layers
            ge.getLayerRoot().enableLayerById(ge.LAYER_BORDERS, true);
            ge.getLayerRoot().enableLayerById(ge.LAYER_ROADS, true);

            //start at some known position
            lookatNewPosition();
        }


        function increaseLatitude() {
            if (currentLatitude < 90) {
                currentLatitude = currentLatitude + 1;
                lookatNewPosition();
            }
            else {
                currentLatitude = -90;
                lookatNewPosition();
            }
        }

        function decreaseLatitude() {
            if (currentLatitude > -90) {
                currentLatitude = currentLatitude - 1;
                lookatNewPosition();
            }
            else {
                currentLatitude = 90;
                lookatNewPosition();
            }
        }

        function increaseLongitude() {
            if (currentLongitude < 180) {
                currentLongitude = currentLongitude + 1;
                lookatNewPosition();
            }
            else {
                currentLongitude = -180;
                lookatNewPosition();
            }
        }

        function decreaseLongitude() {
            if (currentLongitude > -180) {
                currentLongitude = currentLongitude - 1;
                lookatNewPosition();
            }
            else {
                currentLongitude = 180;
                lookatNewPosition();
            }
        }

        function lookatNewPosition() {
            try {

                var flySpeed = isShowingSky ? skyFlySpeed : earthFlySpeed;
                var altitudeType = isShowingSky ? ge.ALTITUDE_ABSOLUTE : ge.ALTITUDE_RELATIVE_TO_GROUND;

                var oldFlyToSpeed = ge.getOptions().getFlyToSpeed();
                ge.getOptions().setFlyToSpeed(flySpeed);  // Slow down the camera flyTo speed.
                var la = ge.createLookAt('');
                la.set(
                    currentLatitude,
                    currentLongitude,
                    currentAltitude, // altitude
                    altitudeType,
                    0, // heading
                    0, // straight-down tiltE
                    0 // range (inverse of zoom)
                );
                ge.getView().setAbstractView(la);
            }
            catch (err) {
                
            }
        }

        function failureCB(errorCode) {
        }

        function showSky() {
            if (ge == undefined)
                return;

            setCommonOptions();
            ge.getOptions().setMapType(ge.MAP_TYPE_SKY);
            isShowingSky = true;
        }

        function showEarth() {
            if (ge == undefined)
                return;

            setCommonOptions();
            ge.getOptions().setMapType(ge.MAP_TYPE_EARTH);
            isShowingSky = false;
        }


        function setCommonOptions() {
            var options = ge.getOptions();
            options.setStatusBarVisibility(true);
        }

    </script>
</head>
<body onload="init();" style="background-color: black; overflow: hidden">
    <div style="position: relative;">
        <div id="map3d" style="background-color: black; height: 490px; width: 490px; margin-left: auto; margin-right: auto; overflow: hidden"></div>
    </div>
</body>
</html>

The most important parts from this JavaScript are the the following methods

showSky() / showEarth()

Called whenever the LightSensor value changes enough to warrant us issuing a call to tell the embedded Google Earth plugin to change its currently shown MapType from Sky to Earth and vice versa

increaseLatitude() / decreaseLatitude()

Called whenever the Gyrometer value changes enough to warrant us issuing a call to tell the embedded Google Earth plugin to alter its current latitude

increaseLongitude() / decreaseLongitude()

Called whenever the Gyrometer value changes enough to warrant us issuing a call to tell the embedded Google Earth plugin to alter its current longitude

LightSensor Code

The LightSensor starts with a UI Service which I have created which is shown in full below

[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Runtime, typeof(ILightSensorService))]
public class LightSensorService : ILightSensorService
{
    private uint defaultUpdateInterval = 1000;
    private LightSensor lightSensor;
    private IObservable<float> lightObservable;

    public LightSensorService()
    {
        lightSensor = LightSensor.GetDefault();
        lightSensor.ReportInterval = defaultUpdateInterval;
        lightObservable = Observable.FromEventPattern<LightSensorReadingChangedEventArgs>(lightSensor, "ReadingChanged")
            .DistinctUntilChanged()
            .Throttle(TimeSpan.FromMilliseconds(500))
            .ObserveOn(new SynchronizationContextScheduler(DispatcherSynchronizationContext.Current))
            .Select(x => x.EventArgs.Reading.IlluminanceInLux);
    }

    public IObservable<float> LightObservable
    {
        get
        {
            return lightObservable;
        }
    }
}

It can be seen that we simply use the Reactive Extensions (Rx) to get an IObservable<float> from the LightSensor.ReadingChanged. We then throttle this reading such that we only get updates every 500 milliseconds and we tell it to ObserveOn the SynchronizationContextScheduler/DispatcherSynchronizationContext (basically the current UI thread). We also expose the IObservable<float> such that users can subscribe to it. So that is the LightSensorService wrapped up. Let's continue to se how we can use this service shall we


We actually use this LightSensorService in the MainWindowViewModel as follows

[ExportViewModel("MainWindowViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class MainWindowViewModel : ViewModelBase
{
    private IViewAwareStatusWindow viewAwareStatusService;
    private IMessageBoxService messageBoxService;
    private IDisposable lightSensorSubscripton;
    .......
    .......

    [ImportingConstructor]
    public MainWindowViewModel(
        IViewAwareStatusWindow viewAwareStatusService, 
        IMessageBoxService messageBoxService, 
        ILightSensorService lightSensorService,
        )
    {
        this.viewAwareStatusService = viewAwareStatusService;
        this.viewAwareStatusService.ViewWindowClosing += viewAwareStatusService_ViewWindowClosing;
        this.messageBoxService = messageBoxService;
        lightSensorSubscripton = lightSensorService.LightObservable.Subscribe(LightChanged, LightException);
        .......
        .......
    }

    void viewAwareStatusService_ViewWindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        lightSensorSubscripton.Dispose();
        gyrometerSensorSubscription.Dispose();
    }

    private void LightChanged(float reading)
    {
        SensorLightMode mode = reading > GlobalSettings.ValueToBeConsideredDark ? 
		SensorLightMode.Light : SensorLightMode.Dark;
        ((ISupportBrowserChanges)this.viewAwareStatusService.View).ApplyChangeBasedOnLightReading(mode);
    }
        
    private void LightException(Exception lightReadingEx)
    {
        Console.WriteLine("Oh no the light is not reading any more");
    }

    .......
    .......
    .......
    .......
}

It can be seen that we simply subscribe to the LightSensorServices IObservable<float> and we then examine the value and compare it to one of the magical GlobalSettings that we talked about earlier, and then we come up with a new SensorLightMode enum value, and then we use some code that actually talks to the View though a ISupportBrowserChanges interface shown below (This is thanks to some magic that I get for free my using my own Cinch MVVM Framework).

interface ISupportBrowserChanges
{
    void ApplyChangeBasedOnLightReading(SensorLightMode sensorLightMode);
    void IncreaseLatitude();
    void DecreaseLatitude();
    void IncreaseLongitude();
    void DecreaseLongitude();
}

Where we use the ApplyChangeBasedOnLightReading(..) method to actually get the embedded embedded Google Earth plugin to show the Earth/Stars

public partial class MainWindow : Window, ISupportBrowserChanges
{
    private Dictionary<SensorLightMode, LightSensorMetadata> sensorLightToSkinLookup = new Dictionary<SensorLightMode, LightSensorMetadata>();
    private SensorLightMode? currentSensorLightMode = null;

    public MainWindow()
    {
        InitializeComponent();
        this.Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        SetupLightSensorLookups();
        SetupWebBrowser();
    }

    private void SetupWebBrowser()
    {
        string s = this.GetType().Assembly.Location;
        FileInfo assFile = new FileInfo(this.GetType().Assembly.Location);
        string htmlContents = File.ReadAllText(string.Format(@"{0}\GoogleEarth\{1}", assFile.Directory.FullName, "GE.htm"));
        webViewer.NavigateToString(htmlContents);
    }
      
    private void SetupLightSensorLookups()
    {
        sensorLightToSkinLookup.Add(SensorLightMode.Dark, new LightSensorMetadata(@".\Resources\Skins\DarkSkin.xaml", "showSky"));
        sensorLightToSkinLookup.Add(SensorLightMode.Light, new LightSensorMetadata(@".\Resources\Skins\LightSkin.xaml", "showEarth"));
    }

    public void ApplyChangeBasedOnLightReading(SensorLightMode sensorLightMode)
    {
        if (!sensorLightToSkinLookup.Any()) 
            return;

        ApplySkinBasedOnLightReading(sensorLightMode);
        ShowGoogleObjectBasedOnLightReading(sensorLightMode);
    }

    ......
    ......
    ......
    ......
    ......



    private void ApplySkinBasedOnLightReading(SensorLightMode sensorLightMode)
    {
        if (currentSensorLightMode.HasValue && sensorLightMode == currentSensorLightMode)
            return;

        currentSensorLightMode = sensorLightMode;

        Debug.WriteLine(string.Format("SensorLightMode {0}", sensorLightMode));
        Uri skinDictUri = new Uri(sensorLightToSkinLookup[sensorLightMode].SkinLocation, UriKind.Relative);
            
        // Tell the Application to load the skin resources.
        App app = Application.Current as App;
        app.ApplySkin(skinDictUri);
    }

    private void ShowGoogleObjectBasedOnLightReading(SensorLightMode sensorLightMode)
    {
        InvokeWebBrowserScript(sensorLightToSkinLookup[sensorLightMode].GoogleJavaScriptFunctionToCall);
    }

    private void InvokeWebBrowserScript(string functionName)
    {
        try
        {
            webViewer.InvokeScript(functionName);
        }
        catch (Exception ex)
        {
            //Ooops : Still since this is just a demo and fun with sensors, no problemo
        }
    }
}

Gyrometer Code

The GyrometerSensor starts with a UI Service which I have created which is shown in full below

public class GyrometerReading
{
    public double VelocityX { get; private set; }
    public double VelocityY { get; private set; }
    public double VelocityZ { get; private set; }

    public GyrometerReading(double velocityX, double velocityY, double velocityZ)
    {
        this.VelocityX = velocityX;
        this.VelocityY = velocityY;
        this.VelocityZ = velocityZ;
    }
}


[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Runtime, typeof(IGyrometerSensorService))]
public class GyrometerSensorService : IGyrometerSensorService
{
    private uint defaultUpdateInterval = 500;
    private Gyrometer gyrometerSensor;
    private IObservable<GyrometerReading> gyrometerObservable;


    public GyrometerSensorService()
    {
        gyrometerSensor = Gyrometer.GetDefault();
        gyrometerSensor.ReportInterval = defaultUpdateInterval;
        gyrometerObservable = Observable.FromEventPattern<GyrometerReadingChangedEventArgs>(gyrometerSensor, "ReadingChanged")
            .DistinctUntilChanged()
            .ObserveOn(new SynchronizationContextScheduler(DispatcherSynchronizationContext.Current))
            .Select(x => new GyrometerReading(
                x.EventArgs.Reading.AngularVelocityX, 
                x.EventArgs.Reading.AngularVelocityY, 
                x.EventArgs.Reading.AngularVelocityZ
                ));
    }

    public IObservable<GyrometerReading> GyrometerObservable
    {
        get
        {
            return gyrometerObservable;
        }
    }
}

It can be seen that we simply use the Reactive Extensions (Rx) to get an IObservable<GyrometerReading> from the Gyrometer.ReadingChanged. We then select a new GyrometerReading for every raw GyrometerReadingChangedEventArgs value, and we tell it to ObserveOn the SynchronizationContextScheduler/DispatcherSynchronizationContext (basically the current UI thread). We also expose the IObservable<GyrometerReading> such that users can subscribe to it. So that is the GyrometerSensorService wrapped up. Let's continue to se how we can use this service shall we

We actually use this GyrometerSensorService in the MainWindowViewModel as follows

[ExportViewModel("MainWindowViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class MainWindowViewModel : ViewModelBase
{
    private IViewAwareStatusWindow viewAwareStatusService;
    private IMessageBoxService messageBoxService;
    private IDisposable gyrometerSensorSubscription;
    ......
    ......
    private GyrometerReading previousGyrometerReading = null;

    [ImportingConstructor]
    public MainWindowViewModel(
        IViewAwareStatusWindow viewAwareStatusService, 
        IMessageBoxService messageBoxService, 
        ......
        ......
        IGyrometerSensorService gyrometerSensorService
        )
    {
        this.viewAwareStatusService = viewAwareStatusService;
        this.viewAwareStatusService.ViewWindowClosing += viewAwareStatusService_ViewWindowClosing;
        this.messageBoxService = messageBoxService;
        ......
        ......
        gyrometerSensorSubscription = gyrometerSensorService.GyrometerObservable.Subscribe(GyrometerChanged, GyrometerException);
    }

    void viewAwareStatusService_ViewWindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        lightSensorSubscripton.Dispose();
        gyrometerSensorSubscription.Dispose();
    }

    .....
    .....
    .....
    .....


    private void GyrometerChanged(GyrometerReading reading)
    {
        if(previousGyrometerReading != null)
        {
            ISupportBrowserChanges supportBrowserChanges  = (ISupportBrowserChanges)this.viewAwareStatusService.View;

            double diffLongitude = reading.VelocityX - previousGyrometerReading.VelocityX;
            double diffLatitude = reading.VelocityY - previousGyrometerReading.VelocityY;

            if (Math.Abs(diffLongitude) > GlobalSettings.MinLongitudeChange)
            {
                supportBrowserChanges.IncreaseLongitude();
            }
            else
            {
                supportBrowserChanges.DecreaseLongitude();
            }

            if (Math.Abs(diffLatitude) > GlobalSettings.MinLatitudeChange)
            {
                supportBrowserChanges.IncreaseLatitude();
            }
            else
            {
                supportBrowserChanges.DecreaseLatitude();
            }
        }

        previousGyrometerReading = reading;
    }
        
    private void GyrometerException(Exception gyrometerReadingEx)
    {
        Console.WriteLine("Oh no the gyrometer is not reading any more");
    }
}

It can be seen that we simply subscribe to the GyrometerSensorServices IObservable<GyrometerReading> and we then examine the value and compare it to one of the magical GlobalSettings that we talked about earlier, and if the values have changed enough we then use some code that actually talks to the View though a ISupportBrowserChanges interface that we just discussed above (This is thanks to some magic that I get for free my using my own Cinch MVVM Framework).

Where we use the IncreaseLatitude() / DecreaseLatitude() / IncreaseLongitude() / DecreaseLongitude() methods to actually get the embedded embedded Google Earth plugin to show the alter its Latitude/Longitude

public partial class MainWindow : Window, ISupportBrowserChanges
{
    .....
    .....

    public MainWindow()
    {
        InitializeComponent();
        this.Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        SetupLightSensorLookups();
        SetupWebBrowser();
    }

    private void SetupWebBrowser()
    {
        string s = this.GetType().Assembly.Location;
        FileInfo assFile = new FileInfo(this.GetType().Assembly.Location);
        string htmlContents = File.ReadAllText(string.Format(@"{0}\GoogleEarth\{1}", assFile.Directory.FullName, "GE.htm"));
        webViewer.NavigateToString(htmlContents);
    }

    .....
    .....
    .....
    .....
      
    public void IncreaseLatitude()
    {
        InvokeWebBrowserScript("increaseLatitude");
    }

    public void DecreaseLatitude()
    {
        InvokeWebBrowserScript("decreaseLatitude");
    }

    public void IncreaseLongitude()
    {
        InvokeWebBrowserScript("increaseLongitude");
    }

    public void DecreaseLongitude()
    {
        InvokeWebBrowserScript("decreaseLongitude");
    }

    private void InvokeWebBrowserScript(string functionName)
    {
        try
        {
            webViewer.InvokeScript(functionName);
        }
        catch (Exception ex)
        {
            //Ooops : Still since this is just a demo and fun with sensors, no problemo
        }

    }
}

That's It

Anyway as Forest Gump would say "And that's all I have to say on that"...Seriously now though.....I think the use of Sensors along with Windows 8 could be really really cool. I have barely scratched the surface of what could be done, but I think most of you could agree that by using Sensors we can make our apps more fun, that's for sure. Anyway signing off now, votes and comments are most welcome

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionVery nice. I likes.... PinprotectorPete O'Hanlon29-Jan-13 11:10 
AnswerRe: Very nice. I likes.... PinmvpSacha Barber29-Jan-13 21:54 
GeneralRe: Very nice. I likes.... PinprotectorPete O'Hanlon29-Jan-13 22:09 
GeneralRe: Very nice. I likes.... PinmvpSacha Barber29-Jan-13 23:30 
GeneralRe: Very nice. I likes.... PinprotectorPete O'Hanlon30-Jan-13 0:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140827.1 | Last Updated 29 Jan 2013
Article Copyright 2013 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid