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
{
private static readonly object syncLock = new Object();
private static volatile MySensorManager instance;
private MyAccelerometer accelerometer;
private MyCompass compass;
private MyGeolocation geolocation;
private MyGyrometer gyrometer;
private MyInclinometer inclinometer;
private MyLightSensor lightSensor;
private MySensorManager() { }
public static MySensorManager Instance
{
get
{
if (instance == null)
{
lock (syncLock)
{
if (instance == null)
{
instance = new MySensorManager();
}
}
}
return instance;
}
}
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
{
abstract public class MySensorBase
{
public bool Simulated { get; set; }
public bool Available { get; set; }
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);
}
}
abstract public void TriggerEvent();
public void setSimulated(bool simulated)
{
Simulated = simulated;
OnSimulatedChanged(new SimulatedEventArgs(Simulated));
TriggerEvent();
}
public void setAvailable(bool available)
{
Available = available;
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
{
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; }
private LightSensor lightSensor;
public event ChangeHandler LightReadingChange;
public delegate void ChangeHandler(MyLightSensor sender, LightReadingEventArgs e);
private uint defaultUpdateInterval = 1000;
System.Timers.Timer forceIntialUpdate;
public MyLightSensor()
{
lightSensor = LightSensor.GetDefault();
Minimum = 0;
Maximum = 10000;
if (lightSensor != null)
{
setAvailable(true);
setSimulated(false);
lightSensor.ReportInterval = defaultUpdateInterval;
EventHandling(true);
}
else
{
setAvailable(false);
setSimulated(true);
}
forceIntialUpdate = new System.Timers.Timer(500);
forceIntialUpdate.Elapsed += forceIntialUpdate_Elapsed;
forceIntialUpdate.Start();
}
void forceIntialUpdate_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
TriggerEvent();
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();
}
public void setUpdateIntervalToMinimum()
{
if (lightSensor != null)
{
EventHandling(false);
lightSensor.ReportInterval = lightSensor.MinimumReportInterval;
EventHandling(true);
}
}
public void setUpdateIntervalDefault(uint milliseconds)
{
defaultUpdateInterval = milliseconds;
if (lightSensor != null)
{
EventHandling(false);
lightSensor.ReportInterval = defaultUpdateInterval;
EventHandling(true);
}
}
public uint getMinimumUpdateInterval()
{
if (lightSensor != null)
{
return lightSensor.MinimumReportInterval;
}
else
{
return 1000;
}
}
public uint getCurrentUpdateInterval()
{
if (lightSensor != null)
{
return lightSensor.ReportInterval;
}
else
{
return 1000;
}
}
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();
myLightSensor = MySensorManager.Instance.LightSensor;
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)
{
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 sensor
s 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 = 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 sensor
s 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:
- the vehicle weight
- the Acceleration and
- 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