Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

UltraDynamo (Part 2) - It Is All About the Sensors

0.00/5 (No votes)
17 Jan 2013 1  
In this section, we will take a look at the sensors, the sensor manager and the simulation

Table Of Contents

Note: This is a multi-part article. The downloads are available in Part 1.

Introduction

When I was first developing the application, I simply created a new instance of the sensor I was using as and when required, whether it was on a form or a trend, etc. This was fine just to get the initial application development underway, but quickly proved to be an inefficient and poor design approach. Fortunately, Pete O'Hanlon shared with me his sensor code base he had developed as part of his entry to the competition and this gave me some insights on how to do things properly using a Singleton Pattern.

The singleton pattern (read more here) uses a double check locking mechanism for providing access to the instances of the various sensors, and prevents multi-threaded environments from creating multi-instances of objects that you are trying to maintain a single instance of.

I went away and reworked my sensor code and in the end, have a sensor manager that provides access to all the sensors. Each of the sensors are derived from a base sensor class that has the common functionality, improving code re-use.

Sensor Simulation

As I am developing on machines that do not have the sensors, I also wanted to provided a method of simulating the sensors for testing purposes, etc. The simulation can be enabled at any time, overriding the true sensor values allowing the user (and developer) to test layouts or components without actually needing to have live sensor values.

Another example of why you might want to simulate sensor values is for testing features that you don't necessarily want to have to repeatedly perform as this might put wear and tear on the vehicle or expose personnel to undue repeated risk. Testing software functionality at for example 150 mph repeatedly will eventually get the better of you. So if we can do all the provisional testing without having to leave the comfort of the desk, then it has to be safer for everyone (and less expensive).

The Sensor Manager

The sensor manager provides access to an instance of each of the various sensors. At present, I have included the following sensors:

  • Accelerometer
  • Compass
  • GPS (Geolocation)
  • Gyrometer
  • Inclinometer
  • Ambient Light

Not all functions are fully utilized in the application at present. For example, the Ambient Light sensor can provide a reading to the dashboard sensor overview, but doesn't do anything with it. I do hope to utilize the Ambient Light sensor in the future for maybe changing the colour themes on the displays for maybe day or night operation.

The code below is the sensor manager (MySensorManager) as it currently stands:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UltraDynamo.Sensors
{
    public sealed class MySensorManager
    {
        //Object for SynLocking
        private static readonly object syncLock = new Object();

        //Sensor Manager Instance
        private static volatile MySensorManager instance;

        //Sensor Instances
        private MyAccelerometer accelerometer;
        private MyCompass compass;
        private MyGeolocation geolocation;
        private MyGyrometer gyrometer;
        private MyInclinometer inclinometer;
        private MyLightSensor lightSensor;
       
        //Constructor
        private MySensorManager() { }

        //Sensor Manager Property
        public static MySensorManager Instance
        {
            get
            {
                if (instance == null)
                {
                    lock (syncLock)
                    {
                        if (instance == null)
                        {
                            instance = new MySensorManager();
                        }
                    }
                }

                return instance;
            }
        }

        //Sensor Properties
        public MyAccelerometer Accelerometer
        {
            get
            {
                if (accelerometer == null)
                {
                    lock (syncLock)
                    {
                        if (accelerometer == null)
                        {
                            accelerometer = new MyAccelerometer();
                        }
                    }
                }

                return accelerometer;
            }
        }

        public MyCompass Compass
        {
            get
            {
                if (compass == null)
                {
                    lock (syncLock)
                    {
                        if (compass == null)
                        {
                            compass = new MyCompass();
                        }
                    }
                }

                return compass;
            }
        }

        public MyGeolocation GeoLocation
        {
            get
            {
                if (geolocation == null)
                {
                    lock (syncLock)
                    {
                        if (geolocation == null)
                        {
                            geolocation = new MyGeolocation();
                        }
                    }
                }

                return geolocation;
            }
        }

        public MyGyrometer Gyrometer
        {
            get
            {
                if (gyrometer == null)
                {
                    lock (syncLock)
                    {
                        if (gyrometer == null)
                        {
                            gyrometer = new MyGyrometer();
                        }
                    }
                }

                return gyrometer;
            }
        }

        public MyInclinometer Inclinometer
        {
            get
            {
                if (inclinometer == null)
                {
                    lock (syncLock)
                    {
                        if (inclinometer == null)
                        {
                            inclinometer = new MyInclinometer();
                        }
                    }
                }

                return inclinometer;
            }
        }       
        
        public MyLightSensor LightSensor
        {
            get
            {
                if (lightSensor == null)
                {
                    lock (syncLock)
                    {
                        if (lightSensor == null)
                        {
                            lightSensor = new MyLightSensor();
                        }
                    }
                }

                return lightSensor;
            }
        }
    }
}

The Sensors

The sensors where applicable derive from a base sensor object. This contains common functionality for the sensors. Properties for Available and Simulated, methods for changing these properties and also an abstract method TriggerEvent(), which must be overriden in the actual sensor object. The code below is the MySensorBase object.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UltraDynamo.Sensors
{
    /// <summary>
    /// Contains the base properties, methods and objects that all My.... Sensors will derive from
    /// </summary>
    abstract public class MySensorBase
    {
        /// <summary>
        /// Is the sensor in a simulated state
        /// </summary>
        public bool Simulated { get; set; }

        /// <summary>
        /// Is the real sensor reporting available (i.e. not null when instantiated
        /// </summary>
        public bool Available { get; set; }

        //Events
        public event EventHandler<SimulatedEventArgs> SimulatedChanged;

        protected virtual void OnSimulatedChanged(SimulatedEventArgs e)
        {
            EventHandler<SimulatedEventArgs> handler = SimulatedChanged;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        public event EventHandler<AvailableEventArgs> AvailableChanged;

        protected virtual void OnAvailableChanged(AvailableEventArgs e)
        {
            EventHandler<AvailableEventArgs> handler = AvailableChanged;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        // Methods
        abstract public void TriggerEvent();

        /// <summary>
        /// Switch the simulated mode on or off.
        /// </summary>
        /// <param name="simulated">Switch on (TRUE) or off (FALSE) simulation mode</param>
        public void setSimulated(bool simulated)
        {
            Simulated = simulated;

            //rasie event
            OnSimulatedChanged(new SimulatedEventArgs(Simulated));

            TriggerEvent();
        }

        /// <summary>
        /// Set the status flag for availability of underlying sensor
        /// </summary>
        /// <param name="available">True == Available, False != Available</param>
        public void setAvailable(bool available)
        {
            Available = available;

            //raise event
            OnAvailableChanged(new AvailableEventArgs(Available));

            TriggerEvent();
        }        
    }
}

If we now take a look at one of the sensors, we can get a better idea of what is going on. The code below is the MyLightSensor object, this is the simplest sensor to work with.

At the top of the code, you will see that the object derives from the MySensorBase. This is then followed by some public properties which hold the current used value of the sensor, the simulated value of the sensor and the true raw value from the real world sensor. There are also a couple of other properties which are used to hold the minimum and maximum values of the sensor (this isn't currently used, but is there for when we want to limit trends or displayed values to min/max range).

The TriggerEvent() method is used to package up the required data into an EventArgs object that is then passed with the event to any objects that have subscribed to these events.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.Sensors;

namespace UltraDynamo.Sensors
{
    public class MyLightSensor : MySensorBase
    {
        //Object for SynLocking
        private static readonly object syncLock = new Object();

        public float LightReading {get; private set; }
        public float rawLightReading {get; private set; }
        public float simLightReading {get; private set; }

        public float Minimum { get; set; }
        public float Maximum { get; set; }

        //Source Sensor
        private LightSensor lightSensor;

        //Events
        public event ChangeHandler LightReadingChange;
        public delegate void ChangeHandler(MyLightSensor sender, LightReadingEventArgs e);

        //default update interval (milliseconds)
        private uint defaultUpdateInterval = 1000;

        //Pseudo Initial Event Timer
        System.Timers.Timer forceIntialUpdate;

        public MyLightSensor()
        {            
            //Base sensor
            lightSensor = LightSensor.GetDefault();

            Minimum = 0;
            Maximum = 10000;

            if (lightSensor != null)
            {
                setAvailable(true);
                setSimulated(false);

                //Set update interval
                lightSensor.ReportInterval = defaultUpdateInterval;

                EventHandling(true);
            }
            else
            {
                setAvailable(false);
                setSimulated(true);
            }

            //Pseudo initial event
            forceIntialUpdate = new System.Timers.Timer(500);
            forceIntialUpdate.Elapsed += forceIntialUpdate_Elapsed;
            forceIntialUpdate.Start();
        }

        void forceIntialUpdate_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            //Force the initial event
            TriggerEvent();

            //Stop and dispose of the timer as no longer required.
            forceIntialUpdate.Stop();
            forceIntialUpdate.Dispose();
        }

        void lightSensor_ReadingChanged(object sender, LightSensorReadingChangedEventArgs e)
        {            
            rawLightReading = e.Reading.IlluminanceInLux;

            if (!Simulated)
            {
                setLightLevel(rawLightReading);
            }

            TriggerEvent();
        }

        public override void TriggerEvent()
        {
            var args = new LightReadingEventArgs();
            args.Available = this.Available;
            args.Simulated = this.Simulated;
            args.LightReading = this.LightReading;
            args.RawLightReading = this.rawLightReading;
            args.SimLightReading = this.simLightReading;

            if (LightReadingChange != null)
            {
                LightReadingChange(this, args);
            }
        }

        private void setLightLevel(float value)
        {
                LightReading = value;
        }

        public void setSimulatedValue(float value)
        {
            simLightReading = value;

            if (Simulated)
            {
                setLightLevel(simLightReading);
            }

            TriggerEvent();
        }

        /// <summary>
        /// Set the sensor to highspeed
        /// </summary>
        public void setUpdateIntervalToMinimum()
        {
            if (lightSensor != null)
            {
                EventHandling(false);
                lightSensor.ReportInterval = lightSensor.MinimumReportInterval;
                EventHandling(true);
            }
        }

        /// <summary>
        /// Set the sensor to a default update interval
        /// </summary>
        public void setUpdateIntervalDefault(uint milliseconds)
        {
            defaultUpdateInterval = milliseconds;

            if (lightSensor != null)
            {
                EventHandling(false);
                lightSensor.ReportInterval = defaultUpdateInterval;
                EventHandling(true);
            }
        }

        /// <summary>
        /// Return the minimum update interval that the sensor provides
        /// </summary>
        /// <returns>uint - milliseconds</returns>
        public uint getMinimumUpdateInterval()
        {
            if (lightSensor != null)
            {
                return lightSensor.MinimumReportInterval;
            }
            else
            {
                return 1000;
            }
        }

        /// <summary>
        /// Return the currently set update interval
        /// </summary>
        /// <returns></returns>
        public uint getCurrentUpdateInterval()
        {
            if (lightSensor != null)
            {
                return lightSensor.ReportInterval;
            }
            else
            {
                return 1000;
            }
        }

        /// <summary>
        /// Set the update interval of the sensor
        /// </summary>
        /// <param name="milliseconds"></param>
        public void setUpdateInterval(uint milliseconds)
        {
            if (lightSensor != null)
            {
                if (milliseconds > getMinimumUpdateInterval())
                {
                    EventHandling(false);
                    lightSensor.ReportInterval = milliseconds;
                    EventHandling(true);
                }
            }
        }

        private void EventHandling(bool enable)
        {
            if (enable)
            {
                lightSensor.ReadingChanged += lightSensor_ReadingChanged;
            }
            else
            {
                lightSensor.ReadingChanged -= lightSensor_ReadingChanged;
            }
        }
    }
}

The EventArgs

For each of the sensors that I used, I created a relevant EventArgs class that derived from the base EventArgs. This was used to package the relevant data that I wanted to use and could then pass this data with the events as they were raised. For the sensor in the code above, the light sensor data was passed in LightReadingEventArgs. The class for this is shown below:

public sealed class LightReadingEventArgs : EventArgs
{
    public bool Available { get; set; }
    public bool Simulated { get; set; }

    public float LightReading { get; set; }
    public float RawLightReading { get; set; }
    public float SimLightReading { get; set; }

    public LightReadingEventArgs()
        : base()
    { }

}

As you can see, the data passed contains whether the sensor is available or not and if the sensor is being simulated, along with what the light reading being used is, as well as the raw reading from the sensor and the simulated reading being injected.

Pseudo Event - What Is That For?

If you look at the MyLightSensor constructor, I generate a timer that is set for 500ms, and then start the timer.

The reason for this is quite simple. Throughout the application, the GUI, etc. wait for data change events to occur in the underlying sensors and use the data from the EventArgs to update the GUI. This Pseudo event serves 2 purposes. The first is that rather than write code all over the application to set up the initial displays, etc, I simple fire off an event with the base data, this cuts down on the amount of initialisation code in the forms. The second reason is that the underlying sensors do not raise an event until there has actually been a data change. For example, no events will raise from an Accelerometer until there is actual movement. So, rather than wait for changes to occur before updating the displays, I trigger off the pseudo event with the dummy data. You cannot raise the event whilst still in the constructor, because nothing has been finally constructed yet, hence the reason for a timer. The 500ms duration also ensures that the remaining constructor code will have time to execute before the timer fires (or you would get an exception).

In the timer event handler, I raise the sensor change event which is packed with the dummy data and then stop the timer and dispose of it as it is no longer required.

Update Intervals

Each of the sensors has a update interval that can be set. You do not want to set this value to low as it would result in a flood of event data to be handled. Also, you do not want this to be too high as this would result in 'jerky' data. When each sensor instance is created, I default it to a value that seems reasonable, say 1 second (1000ms). When the application runs, the user can also configure a default update interval. Whenever the update interval is changed, the code also checks to see if this is small that the internal minimum value from the sensors API.

An important factor to be aware of with the sensor is that when changing the update interval, it is important to disable any event handlers before changing the interval and then re-enabling them after. I am not exactly sure why this must be done, but it was quoted in the API.

What If There Is No Sensor Available?

When the constructor gets an instance of the real sensor from the runtime API, if no sensor is available, the API will return null. If this is the case, then the code will automatically set the sensor into simulated mode and mark it as unavailable.

Putting It Together

If we look at the code used on the sensor overview tab of the main dashboard form, we can see how the sensor manager, the sensor and events are all strung together.

In the following code, I have stripped out all the other sensors code and concentrated only on the light sensor for clarity.

In the main form, we declare a field to contain the reference to the sensor:

public partial class FormMain : Form
    {
        MyLightSensor myLightSensor;
    }

In the constructor for the FormMain, we get the instance of the sensor from the sensor manager and load this into the private field. We also then attach a handler for the LightReadingChanged event:

public FormMain()
{
  InitializeComponent();

  //Light Sensor
  myLightSensor = MySensorManager.Instance.LightSensor;
       
  //Light Sensor
  myLightSensor.LightReadingChange += MyLightSensor_LightReadingChange;
}

In the load event for the MainForm, we use the application configuration properties to update the default update interval for the light sensor:

private void FormMain_Load(object sender, EventArgs e)
{
  //Light Sensor
  numericLightSensorUpdate.Value = Properties.Settings.Default.LightSensorDefaultUpdateInterval;
  myLightSensor.setUpdateIntervalDefault(Properties.Settings.Default.LightSensorDefaultUpdateInterval);
        myLightSensor.setUpdateInterval(Properties.Settings.Default.LightSensorDefaultUpdateInterval);

Finally, we provide the method that is executed when the event handler is triggered.

void MyLightSensor_LightReadingChange(MyLightSensor sender, LightReadingEventArgs e)
{
  if (this.InvokeRequired)
  {
    this.BeginInvoke(new MethodInvoker(delegate() 
          {      MyLightSensor_LightReadingChange(sender, e); }));
    return;
  }
  checkLightSensorAvailable.Checked = e.Available;
  checkLightSensorSimulated.Checked = e.Simulated;
  labelLightReading.Text = e.LightReading.ToString("#0.00");

  labelLightSensorUpdateInterval.Text = myLightSensor.getCurrentUpdateInterval().ToString();
  labelLightSensorUpdateIntervalMinimum.Text = myLightSensor.getMinimumUpdateInterval().ToString();
}

One very important factor to notice is that the cross threading calls are checked to see if the code needs to be invoked against the thread that it is going to be executed on. Originally, I had a lot of problems with cross threading issues until this InvokeRequired/BeginInvoke/MethodInvoker calls were made.

Sensor Overview

In the application window, there is a tabpage that provides an status overview and current values of the sensors monitored by the application:

Managing the Simulation

The form below is the one used to simulate the Light Sensor:

public partial class FormSimulateLightSensor : Form
{
    MyLightSensor myLightSensor;

    public FormSimulateLightSensor()
    {
        InitializeComponent();
        //myLightSensor = new MyLightSensor();
        myLightSensor = MySensorManager.Instance.LightSensor;

        trackSimulateValue.Minimum = (int)myLightSensor.Minimum;
        trackSimulateValue.Maximum = (int)myLightSensor.Maximum;

        myLightSensor.LightReadingChange += MyLightSensor_LightReadingChange;

        checkSimulateEnable.Checked = myLightSensor.Simulated;
    }

    void MyLightSensor_LightReadingChange(MyLightSensor sender, LightReadingEventArgs e)
    {
        if (this.InvokeRequired)
        {
            this.BeginInvoke(new MethodInvoker(delegate()
                   { MyLightSensor_LightReadingChange(sender, e); }));
            return;
        }

        checkAvailable.Checked = e.Available;
        checkSimulated.Checked = e.Simulated;
        checkSimulateEnable.Checked = e.Simulated;

        labelRawValue.Text = e.RawLightReading.ToString("#0.00");
        labelUsedValue.Text = e.LightReading.ToString("#0.00");
        labelSimulatedValue.Text = e.SimLightReading.ToString();

        trackSimulateValue.Value = (int)e.SimLightReading;
    }

    private void trackSimulateValue_ValueChanged(object sender, EventArgs e)
    {
        myLightSensor.setSimulatedValue((float)trackSimulateValue.Value);
    }

    private void checkSimulateEnable_CheckedChanged(object sender, EventArgs e)
    {
        myLightSensor.setSimulated(checkSimulateEnable.Checked);
    }
}

You can see in the code above that we create a local private field to hold the reference to the light sensor and get the reference to the sensor from the SensorManager. A handler is also established to execute when the sensor value changes.

When the slider is adjusted, the public set simulated method of the sensor is called to push the value back into the sensor object.

The form also has the check box for changing whether the sensor is running in Simulated or Real mode.

Sensor Fusion

Sensor Fusion was a term coined by Intel to refer to the use of multiple sensors to provide more detailed or useful information. Read about it here [^].

In this application, I had applied my own sensor fusion. In the dashboard display above, you will notice that the right hand dial indicator is displaying Net Horsepower. This is a calculated figure that is based on:

  1. the vehicle weight
  2. the Acceleration and
  3. the vehicle speed

If you refer to the wikipedia article and look at the calculations for the drawbar horsepower, you can see how they relate to each other.

James Watt introduced this unit of power in the 18th century and he stated that 1 horsepower was the power required to lift a 550lb weight in 1 second. It is possible to calculate this using the parameters shown and data from the Ultrabook sensors as:

Net HP = (Weight (in lbs) x Acceleration (g) x Speed (mph) ) / 375

The weight is provided by the user on the configuration page of the application, the other data is sensor derived. The 375 is a constant derived from the conversion of the 550 lb/second to miles per hour.

It would be easy to add in calculated constants for drivetrain losses and drag coefficients, but that is another job for another day!

Known Sensor Issue

During development, a few of use noted that the Geolocation was not providing accurate location data, or providing any altitude or speed values. After various postings on Intels forums and several emails back and forth on their support site, Intel finally acknowledged that there was a fault in the sensor software, however in this version of the development platform, it would not be fixed. This left me in a bit of a pickle, as some of the planned functions were dependent on the GPS functionality.

What the platform was actually providing was triangulated location data based on the WiFi signals, and this wasn't really suitable for my needs.

Configuration Options

There are various configuration options that can be set by the user, namely default sensor update intervals, vehicle and occupant weight (as used by the horsepower calculator). The configuration can be accessed from the configuration tab:

On to the Next Part

In the next section, the Real Time Trend aspects will be the focus of discussion.

Part 3 - Real Time Trends

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here