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 CDT158 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:
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:
- Choose items
- Select Browse
- Navigate to \PIPC\PISDK and select the file OSIsoft.PISDK.Controls.PISDKCtrlDlg.dll
- Now you should be able to add [ServPickList / OSIsoft.PISDK.Controls] to your toolbox
Let’s code!
- Go to File, New Project, Windows Form Application
- Add the following controls to the Main Form
- ServPickList (srvPickList1)
- Button (buttonConnect)
- ComboBox (tagComboBox)
- 2 DateTimePicker (dateTimePickerStart, dateTimePickerStop)
- PictureBox (pictureBox1)
- [optional] Timer (timer1)
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.
- 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:
- 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;
- 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();
- The DateTimePicker controls should show the exact selected date with the seconds... so, let's change their properties to do so:
- 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.
- 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)
- 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;
}
}
- 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);
}
- That's it... now we can run it and see if it works!
- 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();
}
- 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.
Another nice tip. It is important to note that PlotValues does NOT "Get all values from the tag in the pi server" as it says in Step 9. Instead, PlotValues is essentially a means to retrieve the minimum number of values needed to faithfully reproduce the shape of the trend curve for the given plot area. This means PlotValues is optimized for better performance, and is one of the key IP of PI. This functionality allows PI products like ProcessBook, DataLink, and WebParts to quickly and efficiently display trends with many tags and large number of points.