Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / C#

UltraDynamo (Part 3) - Real Time Trends

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
14 Jan 2013CPOL5 min read 23.4K   13   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.

Image 1

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.

C#
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:

C#
public enum RealtimeTrendSource
{
    Compass,
    Accelerometer,
    Gyrometer,
    Inclinometer,
    AmbientLight,
    Speedometer
}

The next part establishes the object deriving from the base UserControl.

C#
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.

C#
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:

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
        //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.

C#
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.

C#
        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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
Scotland Scotland
I have been working in the Oil & Gas Industry for over 30 years now.

Core Discipline is Instrumentation and Control Systems.

Completed Bsc Honours Degree (B29 in Computing) with the Open University in 2012.

Currently, Offshore Installation Manager in the Al Shaheen oil field, which is located off the coast of Qatar. Prior to this, 25 years of North Sea Oil & Gas experience.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Ahmed Ibrahim Assaf27-Jan-13 22:17
professionalAhmed Ibrahim Assaf27-Jan-13 22:17 

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

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