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

UltraDynamo (Part 3) - Real Time Trends

0.00/5 (No votes)
14 Jan 2013 1  
A look at achieving real time trends in UltraDynamo

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; //1 Minutes (10 points per second @100ms/point)

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();

    //Setup the timer
    updateTimer = new Timer();
    updateTimer.Interval = 100;     //100ms update interval
    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)
{
    //Detect parent form closing to terminate the timer
    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: // m/s
                        //do nothing already in m/s
                        break;
                    case 1: //kph
                        speed = (speed * 3600) / 1000;
                        break;
                    case 2: //mph
                        speed = speed * 2.23693629;  //Google says:
                                                     //1 metre / second = 2.23693629 mph
                        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.

        //Remove excess points
        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 = new MyAccelerometer();
            sourceData = MySensorManager.Instance.Accelerometer;
            break;

        case RealtimeTrendSource.AmbientLight:
            //sourceData = new MyLightSensor();
            sourceData = MySensorManager.Instance.LightSensor;
            break;

        case RealtimeTrendSource.Compass:
            //sourceData = new MyCompass();
            sourceData = MySensorManager.Instance.Compass;
            break;

        case RealtimeTrendSource.Gyrometer:
            //sourceData = new MyGyrometer();
            sourceData = MySensorManager.Instance.Gyrometer;
            break;

        case RealtimeTrendSource.Inclinometer:
            //sourceData = new MyInclinometer();
            sourceData = MySensorManager.Instance.Inclinometer;
            break;

        case RealtimeTrendSource.Speedometer:
            //sourceData = new MyGeolocation();
            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()
        {
            //Configure Series
            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;

                    //Preload maxPoints with 0 data 
                    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;

                    //Preload maxPoints with 0 data 
                    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;

                    //Preload maxPoints with 0 data 
                    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;
                    //Preload maxPoints with 0 data 
                    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;
                    //Preload maxPoints with 0 data 
                    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: //m/s
                            chartData.Series["Speedometer"].LegendText = "M/S";
                            break;
                        case 1: //kmh
                            chartData.Series["Speedometer"].LegendText = "KPH";
                            break;
                        case 2: //mph
                            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;
                    //Preload maxPoints with 0 data 
                    for (int x = 0; x < maxPoints; x++)
                    {
                        chartData.Series["Speedometer"].Points.Add(0D);
                    }
                    break;
            }

            //restart the timer
            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

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