Table Of Contents
Note: This is a multi-part article. The downloads are available in Part 1.
Introduction
The application makes use of real time trends to provide historical data trending of sensor values in real-time. The real time trend is essentially a chart control that has a limited number of data points that continually adds new points to the right and deletes old points from the left and then redraws.
I achieved this by creating a UserControl
that has a chart control docked on the user control to fill its available area. The user control is then added to a normal windows form wherever required. I use the same user control for all the different sensors and just modify the inner workings depending on which data is being trended.
In this section, we will look at how real time trends were created in the UltraDynamo
application.
What is a Real Time Trend?
In a nutshell, a real time trend is a chart of constantly changing dynamic data. It shows you the current plotted value and a series of historical data previous values.
The image below is a screen grab from the real time trend for the Accelerometer sensor. It shows the X, Y and Z axis values being plotted on a horizontally scrolling trend. The newest data is on the right, the oldest data is on the left.
What Control is Used?
The main window is a standard windows form. On the form is placed a user control that I created. I used a user control approach so that I could effectively use the user control anywhere in the application, either as a standalone chart window, or a smaller control on a form with a bunch of other controls.
The chart itself is made using the Microsoft MSChart control. It is placed on the usercontrol
and docked to take up the whole area of the user control.
How Does It Generally Work?
The basis of design is quite simple. The user control has its own timer which is used to poll the sensor for the data on a fixed frequency. As each data point is read from the sensor, it is added to the data points on the chart. If we have exceeded the maximum number of data points we want to plot, then we delete the older points to make room for the new points.
My initial real-time trends contain 600 data points per sensor parameter. This comprises 1 minutes data with 10 data points per second sample rate, i.e., every 100ms, I grab a sensor value.
Code Breakdown
Let us now take a look at the code in detail:
The first section is your typical references and namespace
definition.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using UltraDynamo.Sensors;
namespace UltraDynamo.Controls
{
The following enumerator provides the various values that can be passed to initialise the real time trend with the data to be displayed:
public enum RealtimeTrendSource
{
Compass,
Accelerometer,
Gyrometer,
Inclinometer,
AmbientLight,
Speedometer
}
The next part establishes the object deriving from the base UserControl
.
public partial class UITrendHorizontal : UserControl
{
We now define private
variables for the internal timer and set the number of data points we want as a limit.
private Timer updateTimer;
private int maxPoints = 600;
A property to store the enumerator selected source sensor and the actual source data sensor reference:
public RealtimeTrendSource sourceSensor { get; private set; }
private object sourceData;
The user control constructor base adds the handler for the timers tick
event and sets the tick
frequency to 100ms.
public UITrendHorizontal()
{
InitializeComponent();
updateTimer = new Timer();
updateTimer.Interval = 100;
updateTimer.Tick += updateTimer_Tick;
}
The usercontrol
constructor is used to pass in which sensor we want to trend (from the enumerator list). This also calls the base constructor to establish the update timer
.
public UITrendHorizontal(RealtimeTrendSource source)
: this()
{
setSourceSensor(source);
}
When the control is loaded, we attach a handler to the parent forms closing
event. More on this later.
private void UITrendHorizontal_Load(object sender, EventArgs e)
{
this.ParentForm.FormClosing += UITrendHorizontal_FormClosing;
this.Refresh();
}
When the parent form closes, the code below executes. This stops and disposes of the underlying timer. Without this, the timer could fire after the parent form has been disposed, causing a null
reference exception.
void UITrendHorizontal_FormClosing(object sender, FormClosingEventArgs e)
{
updateTimer.Stop();
updateTimer.Dispose();
}
The timer tick is used to check which sensor data is being trended, then read the sensor value and update the relevant points on the chart.
void updateTimer_Tick(object sender, EventArgs e)
{
switch (this.sourceSensor)
{
case RealtimeTrendSource.AmbientLight:
chartData.Series["Light"].Points.Add(((MyLightSensor)sourceData).LightReading);
break;
case RealtimeTrendSource.Compass:
chartData.Series["Compass"].Points.Add(((MyCompass)sourceData).Heading);
break;
case RealtimeTrendSource.Accelerometer:
chartData.Series["AccelerometerX"].Points.Add(((MyAccelerometer)sourceData).X);
chartData.Series["AccelerometerY"].Points.Add(((MyAccelerometer)sourceData).Y);
chartData.Series["AccelerometerZ"].Points.Add(((MyAccelerometer)sourceData).Z);
break;
case RealtimeTrendSource.Gyrometer:
chartData.Series["GyrometerX"].Points.Add(((MyGyrometer)sourceData).X);
chartData.Series["GyrometerY"].Points.Add(((MyGyrometer)sourceData).Y);
chartData.Series["GyrometerZ"].Points.Add(((MyGyrometer)sourceData).Z);
break;
case RealtimeTrendSource.Inclinometer:
chartData.Series["Pitch"].Points.Add(((MyInclinometer)sourceData).Pitch);
chartData.Series["Roll"].Points.Add(((MyInclinometer)sourceData).Roll);
chartData.Series["Yaw"].Points.Add(((MyInclinometer)sourceData).Yaw);
break;
case RealtimeTrendSource.Speedometer:
double speed = (((MyGeolocation)sourceData).Position.Coordinate.Speed ?? 0);
switch (Properties.Settings.Default.SpeedometerUnits)
{
case 0:
break;
case 1:
speed = (speed * 3600) / 1000;
break;
case 2:
speed = speed * 2.23693629;
break;
}
chartData.Series["Speedometer"].Points.Add(speed);
break;
}
After adding the new points, we make sure that we haven't exceeded the display number of points limit, by simply deleting old points until the maximum point limit is reached.
foreach (System.Windows.Forms.DataVisualization.Charting.Series
series in chartData.Series)
{
while (series.Points.Count > maxPoints)
{
series.Points.RemoveAt(0);
}
}
}
This method simply sets the source sensor to the correct one based on the enumerator value passed in at the time of initialisation.
public void setSourceSensor(RealtimeTrendSource source)
{
this.sourceSensor = source;
switch (this.sourceSensor)
{
case RealtimeTrendSource.Accelerometer:
sourceData = MySensorManager.Instance.Accelerometer;
break;
case RealtimeTrendSource.AmbientLight:
sourceData = MySensorManager.Instance.LightSensor;
break;
case RealtimeTrendSource.Compass:
sourceData = MySensorManager.Instance.Compass;
break;
case RealtimeTrendSource.Gyrometer:
sourceData = MySensorManager.Instance.Gyrometer;
break;
case RealtimeTrendSource.Inclinometer:
sourceData = MySensorManager.Instance.Inclinometer;
break;
case RealtimeTrendSource.Speedometer:
sourceData = MySensorManager.Instance.GeoLocation;
break;
}
updateTimer.Stop();
BuildChartView();
}
The next method sets up the name of the various plots, the legend, etc. based on the sensor being trended.
Each switch case represents one of the sensors. As we enter the case, we clear out any existing series data from the chart. A new Series is added and the associated legend text is provided. We define the minimum and maximum values for the chart and set the tick interval for the markers on the chart.
When the chart is first being created, we add the maximum number of data points with zero values to give us a chart that is already correctly sized, etc. If we did not do this, the chart would grow steadily as the data was captured and this behaviour didn't look right during development, hence the reason for the preloading of zero data. We also start the timer for the first time.
private void BuildChartView()
{
switch (this.sourceSensor)
{
case RealtimeTrendSource.AmbientLight:
chartData.Series.Clear();
chartData.Series.Add("Light");
chartData.Series["Light"].LegendText = "Lux";
chartData.Series["Light"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyLightSensor)sourceData).Minimum;
chartData.ChartAreas[0].AxisY.Maximum = ((MyLightSensor)sourceData).Maximum;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 1000;
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["Light"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Compass:
chartData.Series.Clear();
chartData.Series.Add("Compass");
chartData.Series["Compass"].LegendText = "Heading";
chartData.Series["Compass"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyCompass)sourceData).MinimumHeading;
chartData.ChartAreas[0].AxisY.Maximum = ((MyCompass)sourceData).MaximumHeading;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 90;
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["Compass"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Accelerometer:
chartData.Series.Clear();
chartData.Series.Add("AccelerometerX");
chartData.Series["AccelerometerX"].LegendText = "X";
chartData.Series["AccelerometerX"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyAccelerometer)sourceData).MinimumX;
chartData.ChartAreas[0].AxisY.Maximum = ((MyAccelerometer)sourceData).MaximumX;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 1;
chartData.Series.Add("AccelerometerY");
chartData.Series["AccelerometerY"].LegendText = "Y";
chartData.Series["AccelerometerY"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyAccelerometer)sourceData).MinimumY;
chartData.ChartAreas[0].AxisY.Maximum = ((MyAccelerometer)sourceData).MaximumY;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 1;
chartData.Series.Add("AccelerometerZ");
chartData.Series["AccelerometerZ"].LegendText = "Z";
chartData.Series["AccelerometerZ"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyAccelerometer)sourceData).MinimumZ;
chartData.ChartAreas[0].AxisY.Maximum = ((MyAccelerometer)sourceData).MaximumZ;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 1;
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["AccelerometerX"].Points.Add(0D);
chartData.Series["AccelerometerY"].Points.Add(0D);
chartData.Series["AccelerometerZ"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Gyrometer:
chartData.Series.Clear();
chartData.Series.Add("GyrometerX");
chartData.Series["GyrometerX"].LegendText = "X";
chartData.Series["GyrometerX"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyGyrometer)sourceData).MinimumX;
chartData.ChartAreas[0].AxisY.Maximum = ((MyGyrometer)sourceData).MaximumX;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
chartData.Series.Add("GyrometerY");
chartData.Series["GyrometerY"].LegendText = "Y";
chartData.Series["GyrometerY"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyGyrometer)sourceData).MinimumY;
chartData.ChartAreas[0].AxisY.Maximum = ((MyGyrometer)sourceData).MaximumY;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
chartData.Series.Add("GyrometerZ");
chartData.Series["GyrometerZ"].LegendText = "Z";
chartData.Series["GyrometerZ"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyGyrometer)sourceData).MinimumZ;
chartData.ChartAreas[0].AxisY.Maximum = ((MyGyrometer)sourceData).MaximumZ;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["GyrometerX"].Points.Add(0D);
chartData.Series["GyrometerY"].Points.Add(0D);
chartData.Series["GyrometerZ"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Inclinometer:
chartData.Series.Clear();
chartData.Series.Add("Pitch");
chartData.Series["Pitch"].LegendText = "Pitch";
chartData.Series["Pitch"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyInclinometer)sourceData).MinimumPitch;
chartData.ChartAreas[0].AxisY.Maximum = ((MyInclinometer)sourceData).MaximumPitch;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
chartData.Series.Add("Roll");
chartData.Series["Roll"].LegendText = "Roll";
chartData.Series["Roll"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyInclinometer)sourceData).MinimumRoll;
chartData.ChartAreas[0].AxisY.Maximum = ((MyInclinometer)sourceData).MaximumRoll;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
chartData.Series.Add("Yaw");
chartData.Series["Yaw"].LegendText = "Yaw";
chartData.Series["Yaw"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = ((MyInclinometer)sourceData).MinimumYaw;
chartData.ChartAreas[0].AxisY.Maximum = ((MyInclinometer)sourceData).MaximumYaw;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 15;
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["Pitch"].Points.Add(0D);
chartData.Series["Roll"].Points.Add(0D);
chartData.Series["Yaw"].Points.Add(0D);
}
break;
case RealtimeTrendSource.Speedometer:
chartData.Series.Clear();
chartData.Series.Add("Speedometer");
switch (Properties.Settings.Default.SpeedometerUnits)
{
case 0:
chartData.Series["Speedometer"].LegendText = "M/S";
break;
case 1:
chartData.Series["Speedometer"].LegendText = "KPH";
break;
case 2:
chartData.Series["Speedometer"].LegendText = "MPH";
break;
}
chartData.Series["Speedometer"].ChartType =
System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
chartData.ChartAreas[0].AxisY.Minimum = 0;
chartData.ChartAreas[0].AxisY.Maximum =
(double)Properties.Settings.Default.SpeedometerVMax;
chartData.ChartAreas[0].AxisY.MajorTickMark.Interval = 10;
for (int x = 0; x < maxPoints; x++)
{
chartData.Series["Speedometer"].Points.Add(0D);
}
break;
}
updateTimer.Start();
}
}
}
Using this approach, it should be relatively easy to add in or amend functionality depending on the source sensors that are available or extend the trending for example to increase the number of data points or frequency of samples.
On to the Next Part
In the next part, we will take a look at some of the graphics and font routines used in the application.
Part 4 - Graphics and Fonts