cescamilla

Wordsize Trends

Blog Post created by cescamilla on Aug 10, 2009

Introduction

Showing data from a PI Server in a trend fashion in PI ProcessBook or Excel (with PI DataLink) is really simple (even for most end-users or at least that is what we like to think, thanks to the great smart clients OSIsoft provides :). However there are some custom devices, graphic sizes and specific graphics that cannot be created with the amazing trend control included in PI ProcessBook - a control that is only available inside PI ProcessBook and (in a different flavor) inside Excel.

 

 There also is the issue of having an effective way to show information about some values in a Word document, or a printed piece of paper. And this is exactly what we will attempt here: say you want to know the values for tag CDT158cdt158.png and its trend for the last 24 hours in an easy to read and easy to understand way inside a text flow (like this one). We'll achieve this programmatically and you can use this code anywhere - from mobile devices to a Word add-in.

Overview

In order to show trend data into a custom size trend we may need to go back to high school and remember how to do a graphical representation of data - the hardest part being to fit and fill the X (horizontal) axis as it is mapped to the 'time' variable. We'll have another problem too: graphics have the 'origin' or 'zero' vector at the bottom-left corner of the image but the graphic devices have the 'origin' or 'zero' vector at the top-left corner, so this has to be taken in consideration before plotting the values (otherwise we will get upside-down graphics).

The Contents of a Graphic

They say an image is worth 1 Megapixel (hehehehe) and here we go, our first image:

 

Trending-programatically-_2D00_-How-to-make-a-graphic-for-real-time-data.png

 

  The blue boxes show the way the computer screen is addressed.
  The orange boxes show the way a trend is interpreted.
  The purple part is the trending area.

 

  We need to keep this in mind as we draw the trend step-by-step.

Connecting to the PI Server

For simplicity sake I'll be using the OSIsoft.PISDK.Controls.ServPickList control, which is part of OSIsoft.PISDK.Controls.PISDKCtrlDlg Assembly and has the SelectedServer.Open() Method and the SelectedServer.Connected Property which we will be calling. If you do not have the PI SDK Control Dialogs in your Visual Studio 2008 toolbox then:

  1. Choose items
  2. Select Browse
  3. Navigate to \PIPC\PISDK and select the file OSIsoft.PISDK.Controls.PISDKCtrlDlg.dll
  4. Now you should be able to add [ServPickList / OSIsoft.PISDK.Controls] to your toolbox

Let’s code!

  1. Go to File, New Project, Windows Form Application
  2. Add the following controls to the Main Form
    • ServPickList (srvPickList1)
    • Button (buttonConnect)
    • ComboBox (tagComboBox)
    • 2 DateTimePicker (dateTimePickerStart, dateTimePickerStop)
    • PictureBox (pictureBox1)
    • [optional] Timer (timer1)
      GeneralForm.png
      The beauty of using the PI SDK's Server Pick List control is that we do not need to do anything else: just 'drag and drop' it into the form and that's all we need to make it work.

  3. Now we want to assign/create the "click" action to the 'buttonConnect'
    • Check if the selected server is connected, open a connection if it is not and enable the tag's control
    • Clear the Tag's control
    • Populate the ComboBox control with all the tags available from the selected server.

private void buttonConnect_Click(object sender, EventArgs e)
{
    //Check if the SelectedServer is connected, if it is not, then
    //open a connection to the server and enable the tagComboBox control.

    if (!servPickList1.SelectedServer.Connected)
    {
        //open selected server
        servPickList1.SelectedServer.Open("");
        //enable tag combobox.
        tagComboBox.Enabled = true;
    }

    //Clear the tagComboBox (in case it is not the first click we get)
    tagComboBox.Clear();

    //Populate the ComboBox with all the tags available from the selected server
    foreach (PISDK.PIPoint tmpPoint in servPickList1.SelectedServer.PIPoints)
    {
        //Add point name to tag combo box
        tagComboBox.Items.Add(tmpPoint.Name);
    }
}

 

The result should look like this:
step_2D00_1_2D002D002D00_connect.png

  1. We need to set the date of the date pickers so let's set the start time to 24 hours ago (*-1d) and the stop time to now (*). Note that we could make it the "now" change dynamically but this is beyond the scope of this article.

    Add the following lines in the Form1's "Load" event:

//Set the start date to 24 hours ago
dateTimePickerStart.Value = DateTime.Now.AddDays(-1);
//Set the stop date to now
dateTimePickerStop.Value = DateTime.Now;

  1. Call In the ComboBox we will need to change the "SelectedIndexChanged" event and add a function called update_trend() which we will define in the following step, for now it is enough to prototype it like:

private void update_trend(){}

 

//Index changed should contain this:
update_trend();

  1. The DateTimePicker controls should show the exact selected date with the seconds... so, let's change their properties to do so:
    • Change the 'Format' property to 'Custom'
      date_2D00_format_2D00_custom.png
    • Then change the 'CustomFormat' property to something meaningful (I'll use the standard PI format: dd-MMM-yy HH:mm:ss)
      date_2D00_customformat.png
      Make sure you do this for both date time pickers.

  2. Finally (and for the most fun) we need to create a PictureBox (to output the trend we will be creating...), I'll allow the PictureBox to occupy most of the space left on my form and I'll set the 'Anchor' property to all borders (Top, Bottom, Left, Right) this will make the image resize as the user resizes the window.
    picturebox.png
  3. Now we need some global variables:
    (integers) XX and YY (size of the bitmap in pixels - horizontally and vertically, respectively)
    (Bitmap) mybitmap - The image we will be using and setting to the pictureBox1
    (Grpahics) myGraphic - Encapsulator, I'll use this to expose extra methods (DrawLine, FillRectangle, Clear)

  int XX = 640; //Horizontal resolution
  int YY = 480; //Vertical resolution
  Bitmap myBitmap; //The bitmap image I'll be using to display the trend
  Graphics myGraphic; //The graphics device needed to expose
                      //some additional methods (Clear, FillRectangle and DrawLine)

  1. Let's now define the update_trend() method,
    Note that this method may be a bit complex if you have never made custom graphics.

    Essentially, the update method needs to:
    • Check if we are connected to the selected PI Server and if the user has selected a tag
    • Create variables for the maximum and minimum values
    • Get the 'zero' and 'span' attributes for this particular tag
    • Determine the dimensions of the trend given the values to graph
    • Create a Graphics object from the Bitmap and clear it with a white background.
    • Set 'zero' to be the Maximum value of zero or pointMin
    • Set 'span' to be the Minimum value of (zero + span) or pointMax
    • Fill a rectangle from (0, span - zero) to (Horizontal size of the bitmap, (Vertical size of the bitmap - zero) / range)
    • Get all necesary values from the tag in the pi server, pair them and draw lines between them if both are numeric values (of type "System.Single")
    • Find the last numerico value from the list and draw a red rectangle at the right end of the graphic with that value
    • Show the bitmap

  private void update_trend()
  {
   //Check if we are connected to the selected server in the server picklist and if
   //the user has selected a tag in the ComboBox
   if (servPickList1.SelectedServer.Connected)
   {
    //Get the tag name from the selected item in the ComboBox
    string tagname = tagComboBox.SelectedItem.ToString();
    //Get the point object
    PISDK.PIPoint point = servPickList1.SelectedServer.PIPoints[tagname];
    //Get values from the PI Server
    PISDK.PIValues myValues = point.Data.PlotValues(dateTimePickerStart.Value,
                              dateTimePickerStop.Value,
                             
XX,
                              null);

    //Variables to help define the height of the trend
    float? NpointMax = null, NpointMin = null;
    float pointMax = 0, pointMin = 0;

    //Get zero from PI Points Database
    float zero = (float)point.PointAttributes["zero"].Value;
    //Get span from PI Points Database
    float span = (float)point.PointAttributes["span"].Value;


    //Check all values to find out the maximum and minimum values
    foreach (PISDK.PIValue tmpValue in myValues)
    {
     //If it has a numeric value
     if (tmpValue.Value.GetType().ToString() == "System.Single")
     {
      //Ff vertical maximum has a value
      if (NpointMax.HasValue)
      {
       //Then keep the highest one (NpointMax or the current item value)
       NpointMax = Math.Max((float)NpointMax, (float)tmpValue.Value);
      }
      else
      {
       //Ff no value, set it to the tag's value
       NpointMax = (float)tmpValue.Value;
      }

      //If vertical minimum has a value
      if (NpointMin.HasValue)
      {
       //Then keep the lowest one (NpointMin or the current item value)
       NpointMin = Math.Min((float)NpointMin, (float)tmpValue.Value);
      }
      else
      {
       //If no value, set it to the tag's value
       NpointMin = (float)tmpValue.Value;
      }
     }
    }

    //if NpointMax is not null
    if (NpointMax.HasValue)
    {
     //then set pointMax to NpointMax
     pointMax = (float)NpointMax;
    }

    //if NpoinMin is not null
    if (NpointMin.HasValue)
    {
     //then set pointMin to Npointmin
     pointMin = (float)NpointMin;
    }

    //Calculate the range
    float range = pointMax - pointMin;
    //Add 3% of Span up
    pointMax += Convert.ToSingle(range * 0.03);
    //Add 3% of Span down
    pointMin -= Convert.ToSingle(range * 0.03);
    //Minimum range is 1, range is always positive
    range = Math.Max(1, pointMax - pointMin);
    //Should be constant used for the graphics part
    long ticks = dateTimePickerStop.Value.Ticks - dateTimePickerStart.Value.Ticks;

    //Encapsulate the Bitmap into a Graphics object so we can use the Draw* methods
    myGraphic = Graphics.FromImage(myBitmap);
    //Set the Graphics background to white (using the encapsulation as a mean)
    myGraphic.Clear(Color.White);

    //Get zero value from PI Database's zero
    zero = Math.Max(zero, pointMin);
    //Get span value from PI Database's span
    span = Math.Min(span, pointMax);

    //Fill the rectangle area that is included in the painted area
    myGraphic.FillRectangle(Brushes.LightCyan,
    //From left, top
    0, YY - (span - pointMin) * YY / range,
    //To right, bottom
    XX, (YY - (zero - pointMin) * YY / range) - (YY - (span - pointMin) * YY / range) );

    //Loop through the values to display them
    for (int x = 1; x < myValues.Count; x++)
    {
     //If this value and the next one are numerical
     if (myValues[x].Value.GetType().ToString() == "System.Single" &&
         myValues[x + 1].Value.GetType().ToString() == "System.Single")
     {
      //Draw a line between two points
      myGraphic.DrawLine(Pens.DarkGray,
       //Current date ticks minus first date ticks times pixels divided by ticks
       (myValues[x].TimeStamp.LocalDate.Ticks - myValues[1].TimeStamp.LocalDate.Ticks) * XX / ticks,
       //Image size vertically minus minimum point minus current value
       //times Image size vertically divided by range

       YY - ((float)myValues[x].Value - pointMin) * YY / range,
       //current date ticks minus next date ticks times pixels divided by ticks
       (myValues[x + 1].TimeStamp.LocalDate.Ticks - myValues[1].TimeStamp.LocalDate.Ticks) * XX / ticks,
       //Image size vertically minus minimum point minus next value times Image
       //size vertically divided by range

       YY - ((float)myValues[x + 1].Value - pointMin) * YY / range);
       //The previous lines are necessary to flip, scale, move and translate the
       //graph to an usable region on the graphics device.

     }
    }

    //Find the last numerical value
    for (int  x = myValues.Count; x > 0; x--) {
     if (myValues[x].Value.GetType().ToString() == "System.Single")
     {
      //draw a rectangle in that position
      myGraphic.FillRectangle(Brushes.Red, XX - 2,
                              YY - 2 - ((float)myValues[myValues.Count].Value - pointMin) * YY / range,
                              2, 3);
      break;
     }
    }

    //Set the image source of the picture box to the current bitmap.
    pictureBox1.Image = myBitmap;
   }
  }

  1. Now down to the final details: prepare the trend area when the form loads.

  private void Form1_Load(object sender, EventArgs e)
  {
  //Set the width of the picture box
   XX = pictureBox1.Width;
   //Set the height of the picture box
   YY = pictureBox1.Height;
   //Create an empty bitmap
   myBitmap = newBitmap(XX, YY);
  }

  1. That's it... now we can run it and see if it works!
    Final.png
  2. And... for the final act, you can polish by adding this to the resize function:

private void Form1_Resize(object sender, EventArgs e)
  {
   //Set the width of the picture box
   XX = pictureBox1.Width;
   //Set the height of the picture box
   YY = pictureBox1.Height;
   //Create a new bitmap at least 1 in size
   myBitmap = newBitmap(Math.Max(1,XX), Math.Max(1,YY));
   //Redraw trend
   update_trend();
  }

  1. Optionally, add a (5 seconds?) timer and add this to its 'Tick' event:

private void timer1_Tick(object sender, EventArgs e) {
  //Update the "end" date time picker
  dateTimePickerStop.Value = DateTime.Now;
}

 

Thanks for reading and stay tuned for more!

Last bits

  • As you can see custom trending in your own application/device/console is not difficult.
  • There are a lot of resources for trending and shoThis code does not check for errors, server changes, sign up for updates nor does it handle digital states in tags.
  • This can be used as an image in a web service, or any other kind of application. (More to come about that).. (More to come about that).

References

Last time edits and corrections

  • Andreas suggested the use of PlotValues instead of Recorded values as PlotValues get the least number of values needed to draw a trend at a given horizontal-pixel resolution.
  • Steve suggested the use of Tagsearch instead of a dropdown list for large systems. ComboBox control remained but usability was changed
  • Error checks are not performed
  • Some objects changed their text value (like the button1) sorry about that, the name in code was not changed though.
  • Wait for PART 2 of this post where it is going to be shown how to use and catch digital state values.
  • Project file is attached in the post.

Downloads

Outcomes