Click here to Skip to main content
Click here to Skip to main content

UltraDynamo (Part 3) - Real Time Trends

, 14 Jan 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
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 is the 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 userconstrol contructor used to pass in which sensor we want to trend (from the enumerator list). This also call 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 aleady 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 exceed the display number of points limit, by simply keep 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 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 increases 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)

Share

About the Author

DaveAuld
Engineer
Scotland Scotland
I have been working in the Oil & Gas Industry for over 25 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 for the Beryl Bravo platform, which is located ~180 miles NE of Aberdeen, Scotland in the Northern North Sea.
Formely on the Forties Charlie platform, which is located ~110Miles NE of Aberdeen.
Follow on   Twitter   Google+   LinkedIn

Comments and Discussions

 
GeneralMy vote of 5 PinmemberAhmed Ibrahim Assaf27-Jan-13 23:17 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.1411022.1 | Last Updated 14 Jan 2013
Article Copyright 2013 by DaveAuld
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid