Skip navigation
All Places > PI Developers Club > Blog > Authors andreas
1 2 Previous Next

PI Developers Club

22 Posts authored by: andreas Employee

PI ACE is a nice and convenient tool to create programmable analytics with tags and the Module Database. However – the PI System goes asset centric, so how can we exploit PI ACE to work with PI AF?

 

In the following I would like to show you one way – using the PI AF SDK from PI ACE.

 

First – we want to use the .NET Framework 4 profile, that requires some additional configuration for the PIACEClassLibraryHost executable. You will need to edit/create the PICAEClassLibraryHost.exe.config file located in PIPC\ACE\Scheduler and PIPC\ACE\Scheduler\x86. Here is what you need:

 

<?xml version="1.0"?>
<
configuration>
  
<
startup>
         
<
supportedRuntime version="v4.0" />
   
</
startup>
</
configuration>

 


Let’s start thinking what we want to do. In my simple example, I am going to check all elements below a defined root in a given hierarchy and count the elements where a specific attributes (Common_Alarm) value is “Alarm”. In my case I am going to have an output tag that will receive the result – I leave it to you to write the result to an AF Attribute if you prefer .

 

6622.TRContext01.png

 

Now it’s time to open visual studio and start with our PI ACE Module. We use the above MDB module as the context and the AlarmCount alias as output:

 

 5078.ACEWizard01.png

 

To use the AF SDK – we have to add the reference to the AF SDK and we need the PI SDK as well to access our configuration module, the references should look like this:

 

6685.References01.png

 

Some objects will stay at the class level. These are the objects that we don’t want to initialize at every calculation:

 
    Private myPISystem As OSIsoft.AF.PISystem
    Private myAFDatabase As OSIsoft.AF.AFDatabase
    Private myRootElement As OSIsoft.AF.Asset.AFElement

The configuration stored in the MDB should be initialized only once, and we also want to keep the connection to the AF Server here. So let us look at the ModuleDependentInitialization:

    Protected Overrides Sub ModuleDependentInitialization()

        ' this is the configuration module
        Dim myModule As PISDK.PIModule
        myModule = PIACEBIFunctions.GetPIModuleFromPath(Context)

        Try
            ' Connecting to the PI System
            Dim ppAFServer As PISDK.PIProperty
            ppAFServer = myModule.PIProperties.Item("AFServer")
            myPISystem = New OSIsoft.AF.PISystems().Item(ppAFServer.Value)

            ' Connecting to the AF database
            Dim ppAFDatabase As PISDK.PIProperty
            ppAFDatabase = myModule.PIProperties.Item("AFDatabase")
            myAFDatabase = myPISystem.Databases.Item(ppAFDatabase.Value)

            ' Starting from the root element
            Dim ppRootElement4Calc As PISDK.PIProperty
            ppRootElement4Calc = myModule.PIProperties.Item("RootElement4Calc")
            myRootElement = myAFDatabase.Elements(ppRootElement4Calc.Value)
        Catch ex As Exception
            PIACEBIFunctions.LogPIACEMessage(OSIsoft.PI.ACE.MessageLevel.mlErrors, ex.ToString(),MyBase.Name)
        End Try
    End Sub

 

As mentioned in the beginning, I want to count elements that fulfill a certain condition. Here is my counting function:

 
    Function myCalc(ByVal myRootElement As OSIsoft.AF.Asset.AFElement, ByRef myCount As Integer) As Integer
        ' The Alarm
        Dim iAlarm As Integer
        iAlarm = 0
        ' Try to find a "Common_Alarm" attribute
        Try
            If (myRootElement.Attributes.Item("Common_Alarm").GetValue().Value = "Alarm") Then
                ' Found an alarm
                iAlarm = 1
            End If
            ' Count the number of elements with a Power attribute
            myCount = myCount + 1
        Catch ex As Exception
            ' no "Common_Alarm" attribute, nothing to report
            iAlarm = 0
        End Try
        ' Do the same for every child element and sum up
        For Each myChildElement As OSIsoft.AF.Asset.AFElement In myRootElement.Elements
            iAlarm = iAlarm + myCalc(myChildElement, myCount)
        Next
        ' return the Value
        Return iAlarm
    End Function

Please be aware that it will run recursively through the complete hierarchy below the myRootElement – this could be time consuming and you don’t want to run that on a complex hierarchy every second!

 

So what is left is to call the myCalc function in the ACECalculations. I have included some basic reporting to the message log. This should give you a good indication on how much work your calculation has to do and how long it takes. Finally this should give you some guidance on how often you want to run this :

 
        ' this is for time measurement only
        Dim lStart, lStop As Long

        ' Calculating the sum of all Alarm conditions
        Dim iAlarm As Integer
        iAlarm = 0
        ' Count the elements with the "Common_Alarm" attribute
        Dim iCount As Integer
        iCount = 0

        Try
            ' start of calculation
            lStart = Date.Now.Ticks()
            ' calculate
            iAlarm = myCalc(myRootElement, iCount)
            ' end of calculation
            lStop = Date.Now.Ticks()
            ' report to messagelog
            PIACEBIFunctions.LogPIACEMessage(OSIsoft.PI.ACE.MessageLevel.mlUserMessage, "Alarms:" & iAlarm & "/Count:" & iCount & "/Time:" & ((lStop - lStart) / 1000000.0).ToString("#,###.000"), MyBase.Name)
        Catch ex As Exception
            ' report to messagelog
            PIACEBIFunctions.LogPIACEMessage(OSIsoft.PI.ACE.MessageLevel.mlErrors, ex.ToString(),MyBase.Name)
        End Try

        If (iCount = 0) Then
            ' something wrong, or nothing to report :-(
            SendDataToPI = False
        Else
            ' set the value to the output tag
            AlarmCount.Value = iAlarm
        End If

 

After Rick's post I realized that there are still some code snippets I wanted to share with you. The following snippets will show you simple tasks you used to do with the PI SDK like creating a tag, writing data or signing up for events.

 

 

 

So let's start with connecting to the PI Server:

 
// the PI server
OSIsoft.AF.PI.PIServers myPIServers = new OSIsoft.AF.PI.PIServers();
OSIsoft.AF.PI.PIServer myPIServer = myPIServers.DefaultPIServer;
myPIServer.Connect();

As usual I am using my default server only to keep things simple

 

As Rick mentioned already - things are slightly different between AF SDK and PI SDK. So for the point attributes, AF SDK expects a dictionary:

 

 

 
string strSolution = "AFSDK_RDA_CodeSnippets";
Dictionary<string, object> myAttributes = new Dictionary<string, object>();
myAttributes.Add("CompMin", 0);          // CompMin 0
myAttributes.Add("CompMax", 600);        // CompMax 10 min
myAttributes.Add("CompDev", 60);         // CompDev 60
myAttributes.Add("Compressing", 1);      // Compressing on
myAttributes.Add("ExcMin", 0);           // ExceptionMin 0
myAttributes.Add("ExcMax", 0);           // ExceptionMax 0
myAttributes.Add("ExcDev", 0);           // ExceptionDeviation 0
myAttributes.Add("Pointsource", strSolution);

now we are ready to create the PI point:

 
OSIsoft.AF.PI.PIPoint myPIPoint = myPIServer.CreatePIPoint("myTagname", myAttributes);

Let us generate a list of values: 

 
List<OSIsoft.AF.Asset.AFValue> myValues = new List<OSIsoft.AF.Asset.AFValue>;
DateTime myTimeStamp = DateTime.Now.AddSeconds(-1000);
Random myRandom = new Random(myTimeStamp.Second);

for (int cc = 0; cc < 1000; cc++)
{
        myValues.Add(new OSIsoft.AF.Asset.AFValue(myRandom.Next(0, 100),
                                                  new OSIsoft.AF.Time.AFTime(myTimeStamp)));
        myTimeStamp = myTimeStamp.AddSeconds(1);
}

 and send them to PI:

 

myPIPoint.UpdateValues(myValues, OSIsoft.AF.Data.AFUpdateOption.Insert);

 

 A frequent topic for the PI SDK are the EventPipes. In the AF SDK we have this mechanism as well – the PIDataPipe. This time we are not looking for a single tag, we are getting a PointList:

 

OSIsoft.AF.PI.PIPointList myPIPoints = new OSIsoft.AF.PI.PIPointList(OSIsoft.AF.PI.PIPoint.FindPIPoints(myPIServer, "*", strSolution));

 

 Let us sign up for snapshot events:

 

OSIsoft.AF.PI.PIDataPipe myPIDataPipe = new OSIsoft.AF.PI.PIDataPipe(OSIsoft.AF.Data.AFDataPipeType.Snapshot);
myPIDataPipe.AddSignup(myPIPoints);

 

And for the rest of the day I keep watching events coming into my system:

 

long lValCount = 0;
while (Console.KeyAvailable == false)
{
    OSIsoft.AF.AFListResults<OSIsoft.AF.PI.PIPoint, OSIsoft.AF.Data.AFDataPipeEvent> myEvents;
    myEvents = myPIDataPipe.GetUpdateEvents(1000000);
    if (myEvents.Count > 0)
    {
        lValCount += myEvents.Count;
        Console.WriteLine("{0,7:N0} events ({1,10:N0}) ({2,8:N0} events/s).", 
                          myEvents.Count, 
                          lValCount, 
                          myEvents.Count/2.0);
    }
    System.Threading.Thread.Sleep(2000);
}

So - if you have not tried the new AF SDK 2.5 - go and download the beta and start exploring! 

As a follow up to this post, today I will write a value with annotations to PI.

 

In order to do this, we need the usual preparation. In other words, I need to get my imports:

 
//need to import sdk  
#pragma warning( disable : 4786 4146 )    
#import "E:\PIPC\PISDK\PISDKCommon.dll"    no_namespace 
#import "E:\PIPC\PISDK\PITimeServer.dll"   no_namespace 
#import "E:\PIPC\PISDK\PISDK.dll"          rename("Connected", "PISDKConnected") no_namespace

and some pointers and strings:

 
/* PISDK */
IPISDKPtr       spPISDK = NULL;                /* the PISDK */
ServerPtr       spServer = NULL;               /* the server */
PIPointPtr      spPIPoint = NULL;              /* the pi point */
_bstr_t         bstrServer = "SCHREMMERAVMPI"; /* the pi servername*/
_bstr_t         bstrPointName = "MyLabTag";      /* the tagname */  

now let's start with the initialization:

 
// initialize the COM library
::CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);     

// Create an instance of the PISDK
spPISDK.CreateInstance(__uuidof(PISDK));     
// get the PI server 
spServer = spPISDK->GetServers()->GetItem(bstrServer);     
// get the PI point 
spPIPoint = spServer->PIPoints->GetItem(bstrPointName);       

to make my life easier I am going to use a Float32 tag, and I am assuming it does not have a bad value now. I get the snapshot:

 
// get the current value
_PIValuePtr _pv = spPIPoint->Data->GetSnapshot();

and now I need some annotations:

 
// the PI Annotations 
_PIAnnotationsPtr spPIAnns;
spPIAnns.CreateInstance(__uuidof(PIAnnotations));
     
// a string annotation
spPIAnns->Add(L"Test",L"StringAnnotation",L"MyTest",false,VT_BSTR);
// an integer annotation
spPIAnns->Add(L"Test",L"IntegerAnnotation",(int)1,false,VT_I4);
// a float annotation
spPIAnns->Add(L"Test",L"FloatAnnotation",(float)1.23,false,VT_R4);

As usual we have to deal with VARIANTs:

 
// We need a variant to pass
_variant_t vAnns;
VariantInit (&vAnns);
// the type is VT_DISPATCH
V_VT (&vAnns) = VT_DISPATCH;
// assign the annotations to the variant
V_DISPATCH(&vAnns) = spPIAnns;

Now that there is a VARIANT that refers to my annotations. I am going to use UpdateValues to send my value to PI, and to pass the annotations I will need a named values collection, containing the VARIANT:

 
// named values :-)
_NamedValuesPtr spNVValAttr;
spNVValAttr.CreateInstance(__uuidof(NamedValues));
// add the annotations to the named values
spNVValAttr->Add("Annotations", &vAnns);

almost there now. The remaining part is as simple as creating my PIValues collection, adding my PIValue to it, and sending it to PI. Remember, I am assuming my tag is a Float32 and has no bad value :

 
// PI Values
_PIValuesPtr spPIValues;
spPIValues.CreateInstance(__uuidof(PIValues)); 

// make the PI values writeable
spPIValues->put_ReadOnly(false);
// add a new value with current time, new float value and annotations
spPIValues->Add("*",_pv->Value.fltVal + 1,spNVValAttr);
// make the PI values readonly
spPIValues->put_ReadOnly(true);
// write the value to PI
HRESULT hr = spPIPoint->Data->UpdateValues(spPIValues, dmInsertDuplicates, NULL);

cleaning up: 

 
// we don't need the variant anymore
V_VT (&vAnns) = VT_EMPTY;

spNVValAttr.Release();
spPIAnns.Release();
_pv.Release();spPIPoint.Release();
spServer.Release();
spPISDK.Release();
     
// Closes the COM library
::CoUninitialize();

and leaving:

 
if (hr < 0)         
     return hr;     
else        
     return 0;  

 hope you had as much fun reading as I had writing !

andreas

Compare daily values

Posted by andreas Employee Mar 2, 2012

From time to time people need to compare today's trend for a PI tag with yesterdays, or even several days in the past. One way to achieve this is applying a time offset via a PI Calculation dataset in PI ProcessBook.

 

So if you want to compare yesterday's SINUSOID with todays, you could use a PI Calculation dataset with the formula "TagVal('SINUSOID','*-24h') and trend this along with SINUSOID. Creating such a dataset and adding it to PI ProcessBook is just a couple of lines of VBA code - so please enjoy:

 

First - we have a ProcessBook display with a trend, a button and two combo boxes:

 

0336.TestDS.png

 

To populate the comboboxes at the beginning, let's add some code:

 

Private Sub Display_Open()
    ' Fill in the shifts
    ComboBox_Shifts.Clear
    Dim aa As Integer
    For aa = 1 To 31
        ComboBox_Shifts.AddItem (aa)
    Next
    ' Fill in the offsets
    Combobox_Offset.Clear
    For aa = 1 To 24
        Combobox_Offset.AddItem (aa)
    Next
End Sub

Now we build a procedure to build the dataset. We need the servername, the tagname, the offset and how many times we want to apply the offset:

 

Private Sub CreateAndAddDS(ServerName As String, TagName As String, OffSet As String, OffsetNum As Integer)
    
    Dim MyDataset As PIExpressionDataset
    Dim MyColumn As String
    Dim MyDataSetName As String
    Dim MyDate As Date
    
    ' Find a reasonable name for the dataset
    MyDate = DateAdd("h", -OffsetNum, Trend1.StartTime)
    MyDataSetName = TagName & "_" & MyDate
    MyDataSetName = Replace(MyDataSetName, ".", "_")

    ' Create the dataset
    Set MyDataset = Datasets.Add(MyDataSetName, Nothing, False, 1, True, pbDatasetPIExpression)
    Set MyDataset = Datasets.GetDataset(MyDataSetName)
    
    ' Adjust the dataset to our needs
    MyDataset.ServerName = ServerName
    MyDataset.Expression = "tagval('" + TagName + "','*-" + OffSet + "')"
    MyDataset.RefreshInterval = 3600000
    MyDataset.ColumnName = "Value"
    MyDataset.Interval = "10m"
    Me.Datasets.SetDataset MyDataset
    MyColumn = MyDataset.Name & "." & MyDataset.ColumnName
    ' Add the dataset to the trend
    Trend1.AddTrace MyColumn

End Sub

 

 

What we need to do finally is executing this on button click. But before building the datasets we have to clean the existing ones. Than you can start building the datasets for each tag in the trend:

 

Private Sub CommandButton_Draw_Click()
    
    Dim MyFullTagname As String
    Dim MyServername As String
    Dim MyTagname As String
    Dim MyOffset As String

    Dim aa, bb As Integer
    Dim MyOffsetNum As Integer
    
    MousePointer = pbMPHourGlass
    
    ' Remove all datasets from the trend
    For aa = Trend1.TraceCount To 1 Step -1
        If (InStr(1, Trend1.GetTagName(aa), ".Value")) > 0 Then
            Trend1.RemoveTrace (aa)
        End If
    Next
    
    ' Remove all PIExpression datasets from the display
    Dim MyDS As Dataset
    For Each MyDS In Datasets
        If MyDS.Type = pbDatasetPIExpression Then
            Datasets.Remove (MyDS.Name)
        End If
    Next
        
    ' For all traces in the trend
    For aa = 1 To Trend1.TraceCount
        
        ' Get tagname and servername
        MyFullTagname = Trend1.GetTagName(aa)
        MyServername = Mid(MyFullTagname, 3, InStr(3, MyFullTagname, "\") - 3)
        MyTagname = Right(MyFullTagname, Len(MyFullTagname) - Len(MyServername) - 3)
        
        ' For the number of shifts
        For bb = 1 To (ComboBox_Shifts.Value - 1)
            ' Calculate the "real" offset by
            ' multiplying the shift with the offset
            MyOffset = (bb * Combobox_Offset.Value) & "h"
            MyOffsetNum = (bb * Combobox_Offset.Value)
            
            Call CreateAndAddDS(MyServername, MyTagname, MyOffset, MyOffsetNum)
        Next bb
            
    Next
    
    MousePointer = pbMPDefault

End Sub

 

 

And this is how it looks now:

 

6646.TestDS2.png

 

The complete PB is attached - and I have to mention Dan Fishman and Michael Ramella from field service that initiated all of this.

 

At some point this functionality could be replaced with PI Event Frames (in PI ProcessBook and other client tools). The PM's plan to address this in a more built-in fashion but meanwhile feel free to build on this .

As promised here we will explore more of the PI SDK in C++. This time we are going to use an EventPipe (AKA sign up for events).

 

So we start with our preparation exactly as in this post (Note - the PI SDK has changed since that post, PITimeServer.DLL is now in the PISDK folder), but before we start with _tmain, we need something to handle our event pipe.

 

Here is a little function that takes all events from an EventPipe, counts them and prints them on the command line. We are going to use an EventPipe from a PointList - so the events we are receiving are PointValues, not PIValues.

 
double PrintEvents(EventPipePtr spEvPipe)
{
     // the number of events we are going to read
     long nEvent = 0;

     try 
     {
          while (spEvPipe->Count > 0) 
          {
               // loop through the events
               nEvent++;
               // pointer to the Event object
               _PIEventObjectPtr spEvObj = spEvPipe->Take();
               // the variant to the event data
               _variant_t vT = spEvObj->EventData;
               IDispatchPtr pDispatch = vT.pdispVal;
               // we are signed up for snapshot events, so we get PointValue objects
               PointValuePtr spPV = pDispatch;
               // print the tag, time, value we received in the event
               MyPIValue mPV(spPV->PIValue);
               std::cout << "Eventpipe: ";
               std::cout << spPV->PIPoint->Name << " ";
               std::cout << mPV.bstrTimeStamp << " ";
               std::cout << mPV.bstrValue << std::endl;
          } 
     }
     catch(_com_error Err)
     {
          std::cout << "Error in GetEvent: " 
                    << Err.Description()
                    << " : "
                       << Err.Error()
                      << std::endl
                      << "Unable to load PISDK."
                      << std::endl;
     }
     // return the number of events read
     return nEvent;
}

 Now we can continue with the main function. Similar to last time we initialize COM, check for the command line parameters and finally create the PISDK. After this has been done, we print out the PI SDK version:

 

 

 
int _tmain(int argc, _TCHAR* argv[])
{
     // Initialize COM
     ::CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);
     // Check the command line switches
     if (argc < 3) {
          std::cout << "Command Line:" << std::endl
                      << (_bstr_t)argv[0] << " SERVERNAME TAGNAME(s)";
          return (1);
     }
     try                                    
     {
          // Create an instance of the PI SDK
          spPISDK.CreateInstance(__uuidof(PISDK));
          // Print out the PI SDK version
          spSDKVersion = spPISDK->PISDKVersion;
          std::cout << std::endl << "PI-SDK Version " 
                      << spSDKVersion->Version << " Build "
                      << spSDKVersion->BuildID << std::endl;
          // get the PI Server
          spServer = spPISDK->GetServers()->GetItem((_bstr_t)argv[1]);

 

 

So far everything is similar. But now we need to create a PointList and populate it by the tags we have supplied at the command line:

 
          // we want to use a pointlist
          _PointListPtr spPointList = NULL;
          spPointList.CreateInstance(__uuidof(PointList));
          
          // You can use more than just one tagname
          for (int i = 2; i< argc; i++) {
               // add tags one by one to a pointlist and print out the tags
               std::cout << "Adding " << (_bstr_t)argv [ i ]  << std::endl;
               spPIPoint = spServer->PIPoints->GetItem((_bstr_t)argv [ i ] );
               spPointList->Add(spPIPoint);
          }

          // Print out the number of tags
          std::cout << std::endl;
          std::cout << "Signed up for snapshot evenvts for ";
          std::cout << spPointList->Count;
          std::cout << " tags.";
          std::cout << std::endl;

 

 

Now let's have a look at the EventPipe:

 
          // the EventPipe will handle the events, 
          // the secondary interface is only required if we want to change the defaults
          // like the poll interval
          EventPipePtr     spEvPipe = NULL;             /* the event pipe */
          IEventPipe2Ptr     iep2 = NULL;                  /* Secondary interface */

          // get the eventpipe
          spEvPipe = spPointList->Data->EventPipe;
          // get the secondary interface 
          iep2 = spEvPipe;
          // set the poll intervall to 5000 ms
          iep2->PutPollInterval(5000);

 

 

to finish this simple example I am going to wait for the first ten events in my EventPipe and exit my little application:

 
          // for the sake of simplicity we just wait 
          // for some events in the eventpipe.
          double dblTotCnt = 0;
          while (dblTotCnt < 10)
          {
               Sleep (5000);
               dblTotCnt += PrintEvents (spEvPipe);
          }

     }
     catch( _com_error Err )
     {
          std::cout << "Error: " << Err.Description() << " : " << Err.Error() << std::endl;
          return (1);
     }

     return 0;
}

 

 

Done for this time. Stay tuned for more!

As promised in this post I am continuing my travel to the land of C++.

 

In this post I will try to get a snapshot calling PI Web Services from C++. Similar as with the PI SDK this does requires some more ground work then consuming the PI Web Services in .NET.

 

The following example makes use of the gSoap toolkit, an open source, cross-platform toolkit for calling web services from C and C++ - so you may start by downloading it from here. I encourage you to take a look at the Getting Started. This example requires basic binding with no security - make sure that you use the web_config_basic_no_security web.config file for your PI Webservice*.

 

If you have downloaded the gSoap toolkit you start with creating the include files for your C++ application. This is a two step process starting with using wsdl2h to create the gSOAP header file from the WSDL. Adjust the path to the webservice as necessary. Note that you can choose your own name for the web service namespace with the -n argument.

 

The second step is using the soapcpp2 compiler to create the necessary code snippets to include into your application. The -I indicates the path to the import folder of the gSoap toolkit - you need to adjust that depending of the version and where you have extracted your gSoap toolkit.

 

soapcpp2.exe -1 -i -IE:\gsoap_2.7.17\gsoap-2.7\gsoap\import -C PITimeSeriesService.h

That is the point now for opening Visual Studio and starting our project:

 

 0027.20111118_5F00_01.png

 

Soapcpp2 has created some files that we need in our project:

 

BasicEndpoint.nsmap
soapBasicEndpointProxy.cpp
soapBasicEndpointProxy.h
soapC.cpp
soapH.h
soapStub.h

 

We copy them to our source directory and add the cpp files to the project. Furthermore we need to add stdsoap2.cpp from the gSoap toolkit. Finally it should look like this:

 

1460.20111118_5F00_02.png

 

We need to add the gsoap source directory as additional include directory for the C++ compiler:

 

5822.20111118_5F00_04.png

 

If we have not done so at the beginning we disable precompiled headers now:

 

0451.20111118_5F00_05.png

 

and add the necessary includes:

 
#pragma once
#include
<stdio.h>
#include
<tchar.h>
#include <iostream>
#include "BasicEndpoint.nsmap"
#include "soapBasicEndpointProxy.h"

Now it is time to open our source file and start to write the code. As first step we will get the PI Web Services version and the endpoint:

 

int

_tmain(int argc, _TCHAR* argv[])
{
   
// gSOAP proxy object for PI Web Services
   
BasicEndpointProxy wsService;

    // Product version request

    _PIWS1__GetProductVersion wsVreq;
    _PIWS1__GetProductVersionResponse wsVresp;
   
   
// Get Version and Endpoint string
   
int error = wsService.GetProductVersion(&wsVreq, &wsVresp);

   
if (SOAP_OK == error)
    {
        std::cout <<
"Endpoint: "
                  << wsService.soap_endpoint
                  << std::endl;
        std::cout <<
"PI Web Services Version: "
                  << wsVresp.GetProductVersionResult->c_str()
                  << std::endl;
    }
   
else
    {
        std::cout <<
"GetProductVersion fault\n";
        soap_print_fault(&wsService, stderr);
    }

   
    // free dynamic memory allocated by gSOAP

    wsService.destroy();
   
return 0;
}

Time for testing - if you have done everything above you should get something similar to:

 

2022.20111118_5F00_06.png

 

 Now let us prepare for the snapshot:

 

// Snapshot request
_PIWS1__GetPISnapshotData wsRequest;
_PIWS1__GetPISnapshotDataResponse wsResponse;
PIWS1__ArrayOfString wsPaths;
// Path to a PI Tag
std::string strPath =
"PI:\\\\SCHREMMERAVMPI\\Sinusoid";
// Add to the Array of Paths
wsPaths.string.push_back(strPath);
// Path to a PI Tag
strPath =
"PI:\\\\SCHREMMERAVMPI\\CDT158";
// Add to the Array of Paths
wsPaths.string.push_back(strPath);
// Path to a PI Tag
strPath =
"PI:\\\\SCHREMMERAVMPI\\CDM158";
// Add to the Array of Paths
wsPaths.string.push_back(strPath);
wsRequest.paths = &wsPaths;

And the final step is getting the snapshot and printing it out:

 

error = wsService.GetPISnapshotData(&wsRequest, &wsResponse);


if (SOAP_OK == error)
{
    std::cout <<
"GetPISnapshotData: "
              << wsResponse.GetPISnapshotDataResult->TimeSeries.size()
              << std::endl;
    PIWS1__ArrayOfTimedValue *wsTimedValues;
   
for (unsigned int a=0;
         a < wsResponse.GetPISnapshotDataResult->TimeSeries.size();
         a++)
    {
        wsTimedValues = wsResponse.GetPISnapshotDataResult->TimeSeries[ a ]->TimedValues;
        std::cout << wsResponse.GetPISnapshotDataResult->TimeSeries[ a ]->Path->c_str()
                  <<
" "
                  << ctime(&wsTimedValues->TimedValue[ 0 ]->Time)
                  <<
" "
                  << wsTimedValues->TimedValue[ 0 ]->__item
                  << std::endl;
    }
}
else
{
    std::cout <<
"GetPISnapshotData fault\n";
    soap_print_fault(&wsService, stderr);
}
wsService.destroy();

Again time for a test:

 

5557.20111118_5F00_07.png

 

Done! But not with C++ - we will revisit the PI SDK soon...

 

Notes:

 

*ways to secure basicHttpBinding are described in the PI Web Services documentation.

 

You might recognize STL (Standard Template Library) keywords in the code. gSoap uses this for strings and arrays by default. Good documentation on STL can be found here.

andreas

C++ and PI - the PI SDK

Posted by andreas Employee Nov 23, 2011

Recently we got increasing interest in providing sample code in C++.

 

While C++ is a very powerful language you should be aware that .NET provides you with many little things that make your life as a developer much easier.

 

In the following post I will create a simple command line application that gets the snapshot of tags. The usage is:

 

PISDK_GetSnapshot <SERVERNAME> <Tagname1> <Tagname2> <…>

 

So as you may guess, we start with an empty C++ command line project:

 

2262.image001.png

 

Now let’s go to the code. First some includes:

 

#include "stdafx.h"
#include
<iostream>
#include
<string>
#include
"ATLComTime.h" // for COleDateTime

Then we import the PI SDK libraries.

 

//need to import sdk
#import
"E:\PIPC\PISDK\PISDKCommon.dll"    no_namespace
#import
"E:\PIPC\LIBRARY\PITimeServer.dll" no_namespace
#import "E:\PIPC\PISDK\PISDK.dll"          rename("Connected", "PISDKConnected") no_namespace

One of the things that constantly drives me crazy when switching between C++ and C#.NET is handling the PIValue, especially the variant PIValue->Value – so the very first thing I’d like to do is introducing a simple class that does this for me:

 

class MyPIValue
{
      
_PIValuePtr spPIValue;

public
:
      
MyPIValue (_PIValuePtr);
      
double  dblValue;
      
int     intValue;
      
_bstr_t bstrValue;
      
_bstr_t bstrTimeStamp;
      
COleDateTime codtTimeStamp;
      
VARTYPE vt;
};

The purpose of my simple class is to get variables that handle much better than the variant (at least in my simple sample code snippets).

 

Now let’s go to the constructor. Actually we need to figure where to find the data – and that is derived by the variant type. The following code gets me first a COleDateTime and a string representation of that – this is the timestamp, and later the value part as integer, double or string representation:

 

MyPIValue::MyPIValue (_PIValuePtr pv) {
      
codtTimeStamp = pv->TimeStamp->LocalDate;
      
bstrTimeStamp = (_bstr_t)codtTimeStamp.Format(_T("%d-%b-%Y %H:%M:%S"));
      
DigitalStatePtr tmpDigitalState = NULL;
      
IDispatchPtr    tmpDispatch = NULL;
      
_PITimePtr      tmpPITime = NULL;
      
COleDateTime    tmpTS;
      
HRESULT         hr = E_FAIL;

      
_variant_t vT = pv->Value;
      
vt = vT.vt;

      
switch (vT.vt) {
      
case VT_I4:
             
// Int32
             
intValue = vT.lVal;
             
dblValue = intValue;
             
bstrValue = (_bstr_t)intValue;
             
break;
      
case VT_I2:
             
// Int16
             
intValue = vT.iVal;
             
dblValue = intValue;
             
bstrValue = (_bstr_t)intValue;
             
break;
      
case VT_R8:
             
// Float64
             
dblValue = vT.dblVal;
             
intValue = (int)dblValue;
             
bstrValue = (_bstr_t)dblValue;
             
break;
      
case VT_R4:
             
// Float16/Float32
             
dblValue = vT.fltVal;
             
intValue = (int)dblValue;
             
bstrValue = (_bstr_t)dblValue;
             
break;
      
case VT_BSTR:
             
// String
             
bstrValue = vT.bstrVal;
             
dblValue = 0;
             
intValue = 0;
             
break;
      
case VT_DISPATCH:
             
// Digital?
             
tmpDispatch = vT.pdispVal;
             
hr =  tmpDispatch.QueryInterface(__uuidof(DigitalState),&tmpDigitalState);
             
if (hr == S_OK) {
                    
bstrValue = tmpDigitalState->Name;
                    
intValue = tmpDigitalState->Code;
                    
dblValue = intValue;
             
}
             
// Timestamp?
             
hr =  tmpDispatch.QueryInterface(__uuidof(_PITime),&tmpPITime);
             
if (hr == S_OK) {
                          
tmpTS = tmpPITime->LocalDate;
                          
bstrValue = (_bstr_t)tmpTS.Format(_T("%d %B %Y %H:%M:%S"));
                          
intValue = 0;
                          
dblValue = 0;
             
}
             
break;
      
default :
             
dblValue = 0.0;
             
intValue = 0;
             
bstrValue = "n/a";
             
break;
      
}

};

Preparation doneJ!

 

As mentioned in the beginning, I want to get the snapshot – so what do I need? The PISDK, the Server, the Point, the Value and just for the fun the PISDK Version J:

 

IPISDKPtr       spPISDK = NULL;            /* The PISDK */
PISDKVersionPtr spSDKVersion = NULL;       /* PI SDK Version */

ServerPtr       spServer = NULL;           /* The Server */
PIPointPtr      spPIPoint = NULL;          /* The PI Point */
_PIValuePtr     spPIValue = NULL;          /* The PI value */

Now the code – we initialize COM, check for the command line parameters and finally create the PISDK. After this has been done, we print out the PI SDK version, connect to PI and print out the snapshot of all tags provided as command line arguments:

 

int _tmain(int argc, _TCHAR* argv[])
{
      
// Initialize COM
      
::CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);
      
// Check the command line switches
      
if (argc < 3) {
             
std::cout << "Command Line:" << std::endl
                     
  << (_bstr_t)argv[0] << " SERVERNAME TAGNAME(s)";
             
return (1);
      
}
      
try                                   
      
{
             
// Create an instance of the PI SDK
             
spPISDK.CreateInstance(__uuidof(PISDK));
             
// Print out the PI SDK version
             
spSDKVersion = spPISDK->PISDKVersion;
             
std::cout << std::endl << "PI-SDK Version "
                     
  << spSDKVersion->Version << " Build "
                     
  << spSDKVersion->BuildID << std::endl;
             
// get the PI Server
             
spServer = spPISDK->GetServers()->GetItem((_bstr_t)argv[1]);
             
// You can use more than just one tagname
             
for (int ii = 2; ii< argc; ii++) {
                    
// Tagname
                    
std::cout << (_bstr_t)argv[ii] << std::endl;
                    
spPIPoint = spServer->PIPoints->GetItem((_bstr_t)argv[ii]);
                    
// Snapshot
                    
spPIValue = spPIPoint->Data->Snapshot;
                    
MyPIValue mPV(spPIValue);
                    
std::cout << mPV.bstrTimeStamp << " ";
                    
std::cout << mPV.bstrValue << std::endl;
             
}
      
}
      
catch( _com_error Err )
      
{
             
std::cout << "Error: "
                        << Err.Description()
                        << " : "
                        << Err.Error()
                        << std::endl;
             
return (1);
      
}
      
return 0;

}

And here is the result:

 

4572.image002.png

 

This was the first post - stay tuned for more examples using C++!

andreas

The IDataProvider3

Posted by andreas Employee Nov 1, 2011

Have you ever thought about an IDataProvider3 Add In that exposes more than just a single trace? In this blog post we will fill some example code into the Add-in to help you achieve this.

 

First we start using the AddInPB_IDP3_CS template:

 

20111031_5F00_01.png

 

Note that I am using Visual Studio 2010 and set the .NET Framework 2.0.

 

As a good practice let's rename the menu item. We open IDP3Shell.cs and expand the IDataProvider3_configuration_methods region. We locate PBObjLib.IDataProvider3.GetMenuItem and change the string to something meaningful:

 
string PBObjLib.IDataProvider3.GetMenuItem()
{
    return "MyMultiTrace";
}

Now as we want to get multiple traces we need to add them to the cols collection in bool PBObjLib.IDataProvider3.ShowColumnConfiguration:

 
bool PBObjLib.IDataProvider3.ShowColumnConfiguration(ref PBObjLib.Columns cols, PBObjLib.DataPoint dp)
{
    cols.Add(this.dataSetName + ".RandomData");
    cols.Add(this.dataSetName + ".SinusData");
    return true;
}

Let's collapse the IDataProvider3_configuration_methods region and expand the IDataProvider3_optional_methods region. We are going to add a check on the two columns here as well:

 
bool PBObjLib.IDataProvider3.IsColumnValid(PBObjLib.DataPoint dp)
{
    if (dp == null) 
        return false;
    if (String.Compare(dp.ColumnName, "RandomData", true) == 0) 
        return true;
    if (String.Compare(dp.ColumnName, "SinusData", true) == 0) 
        return true; 
    else return false; 
}

We take care of the configuration later and focus on the data now. Let us collapse the IDataProvider3_optional_methods region and expand the IDataProvider3_data_methods region.

 

To tell PI ProcessBook some necessary information we need to visit PBObjLib.IDataProvider3.GetColumnAttributes. According to the data we are going to change it to:

 
// Set type to Float32
type.Value = PISDK.PointTypeConstants.pttypFloat32;
 
// Check external data provider to determine what the attributes should be
// for the shell dataset, using constants
if (String.Compare(dp.ColumnName, "RandomData", true) == 0)
{
    zero.Value = 3.0;
    span.Value = 0.3;
}
 
if (String.Compare(dp.ColumnName, "SinusData", true) == 0)
{
    zero.Value = -1.0;
    span.Value = 2.0;
}

Keeping it simple we return some random data and a sine wave. So let's go to PBObjLib.IDataProvider3.GetData and do some checks:

 
// Limit the amount of values to 100
if (maxvalues > 100)
    maxvalues = 100;

// Limit the amount of values to 1 value per second
if (maxvalues > (endtime - starttime))
    maxvalues = (endtime - starttime);

 then we add a random and a double:

 
Random random = new Random();
double num = random.Next(100) / 1000.0;

and we extend the for loop:

 
// for the random column
if (String.Compare(dp.ColumnName, "RandomData", true) == 0)
{
    num = 3.1415 +  0.001 * random.Next(100);
}

// for the sinus column
if (String.Compare(dp.ColumnName, "SinusData", true) == 0)
{
    num = Math.Sin(4 * 3.1415 * i / maxvalues);
}
pivalues.Add(timePT, num, attrs);

Now - to debug that thing we need to set the path to PI ProcessBook by a right click on the project:

 

20111031_5F00_02.png

 

Ready to hit F5 and build a trend!

 

20111031_5F00_03.png

 

While this works fine on a trend - a value symbol will always show only the first trace. So let us add a Windows Form to choose what trace we want to show. Two CheckBoxes and two buttons should be enough:

 

20111031_5F00_04.png

 

Let us define some variables for later usage:

 
// the columns
public  PBObjLib.Columns mCols { get; set; }
// the dataset name
public  String mDataSetName { get; set; }
// the application object
public PBObjLib.Application mApp { get; set; }

When activating the form we update the text on the checkboxes

 
DS_random.Text = mDataSetName + ".RandomData";
DS_sinus.Text = mDataSetName + ".SinusData";

and also check what datasets we already use

 
if (mApp.ActiveDisplay.SelectedSymbols.Count > 0)
{
    // get the active symbol
    PBObjLib.Symbol mSymbol = mApp.ActiveDisplay.SelectedSymbols.Item(1);
    // iterate through all tags
    for (int i = 1; i <= mSymbol.PtCount; i++)
    {
        if (String.Compare(mSymbol.GetTagName(i), DS_random.Text, true) == 0)
        {
            DS_random.Enabled = false;
        }
        if (String.Compare(mSymbol.GetTagName(i), DS_sinus.Text, true) == 0)
        {
            DS_sinus.Enabled = false;
        }
    }
}

This is necessary as the column collection only contains the selected column when opened from an existing trend definition.

 

Now we have to fill the ok and the cancel button:

 
private void btnOK_Click(object sender, EventArgs e)
{
    // here we add the columns to the data set
    if (DS_random.Checked)
    {
        mCols.Add(mDataSetName + ".RandomData");
    }
    // here we add the columns to the data set
    if (DS_sinus.Checked)
    {
        mCols.Add(mDataSetName + ".SinusData");
    }
    this.Close();
}

private void btnCancel_Click(object sender, EventArgs e)
{
    this.Close();
}

To make use of this - lets go back to PBObjLib.IDataProvider3.ShowColumnConfiguration and use the form:

 
bool PBObjLib.IDataProvider3.ShowColumnConfiguration(ref PBObjLib.Columns cols, PBObjLib.DataPoint dp)
{
    // This is the configuration form. It is used to choose the columns
    AS_IDP3_MT.ConfigDS MyForm = new AS_IDP3_MT.ConfigDS();
    MyForm.mCols = cols;
    MyForm.mApp = this.app;
    MyForm.mDataSetName = this.dataSetName;
    MyForm.ShowDialog();
    cols = MyForm.mCols;
    return true;
}

Time to hit F5 again:

 

 20111031_5F00_05.png

 

And we can select the two different datasets in our Add-In!

andreas

Exporting to SVG

Posted by andreas Employee Sep 23, 2010

Those of you that have installed and used PI ActiveView know the EXPORT.PDI that is shipped with PI ActiveView and located in .\PIPC\ACVIEW\WebDev. This PDI provides a nice way to load a PIW file and export this file to PDIs.

 

I have modified the EXPORT.PDI from the PI ActiveView distribution to save to SVG. The EXPORT_SVG.PDI does recognize unsupported controls and removes them prior to saving. Please note that his is work in progress, I already have a list of enhancements I need to add.

Hurry up and register today to save money (Early Bird registration ends July 21st - that is today!)

 

All the details and the registration at www.osisoft.com/vCampusLive2010... looking forward to see you at the Palace Hotel!

 

(interested in presenting something at the event - there are 2 more spots open! submit your paper in this discussion thread)

 

960x160_5F00_vCampus2010_5F00_v3.jpg

Overlay a characteristic curve in an XY Plot

Some time ago I got a request to display a characteristic curve on an XY plot. A characteristic curve is an idealized graphical representation of two dependent (physical) measurements. An example of this is the current–voltage characteristic of a diode.

 

In this particular case the request was a best practice curve to appear on an XY plot along with the normal PI data. This had to be done for a wind turbine and should show 'wind speed' versus 'power generated' as PI data and as an idealized wind speed to power relationship.

 

As you know all data in PI archives are stored as time series, so I thought I am out of luck. Overlaying a simple graphic would work, but what happens when the user zooms in? In this little example we will use VBA to overlay an XY plot with dynamic PI data and an XY plot with static data, keeping them overlaid while you zoom and change the time range.

 

XYPic01.jpg

 

Talking to my “relational” colleague Frank caused him to come up with the characteristic curve stored in an access database, just a column for pressure and temperature as an example:

Line

Pressure

Temperature

0

1

5

10

10

17

15

20

20

20

25

22

30

23

35

27

40

30

45

32

50

35

55

45

60

65

65

75

70

85

75

85

80

80

85

75

90

72

95

70

100

70

And use that as a data source for an XY plot. For convenience we create a System Data source in Control Panel>All Control Panel Items>Administrative Tools>Data Sources (ODBC):

 

XYPic02.jpg

 

After this we can simply use it in PI ProcessBook to create a new ODBC Dataset:

 

XYPic03.jpg XYPic04.jpg XYPic05.jpg

 

And we can build a XY plot from this:

 

XYPic06.jpg XYPic07.jpg

 

Next to this plot we are going to create an XY plot with SINUSOID and SINUSOIDU:

 

XYPic08.jpg XYPic09.jpg

 

That was easy. Now we could set the background of our SINUSOID XY Plot to none and carefully move it above the characteristic curve – but this would only be slightly better than a simple bitmap in the background of the trend. So here is how vCampus comes in – let us automate the movement of the trends!

 

We assume, that we have renamed the left XY plot to XYPlotChar (for the left one, the ODBC Data) and the right one to XYPlotReal (for the right one, the PI Data)

 

First we add two command buttons to the display. For demonstrational purposes we want to overlay and separate the two trends:

 

XYPic10.jpg

Now let’s take a look at the code:

    1. First make sure that we do define all variables:

' Define all variables

Option Explicit

 

    1. As there is no VB color code for none, define one:

' We do not have a standard color code for none

Const vbNone = -1

 

    1. Limit the amount of code, so going for one SUB needs a switch:

' Switch between overlay and separate

Const myOverlay = 0

Const mySeparate = 1

Dim myButton As Boolean

 

    1. The myButton is necessary later, so let us initialize it properly:

' Initialization

Private Sub Display_Open()

   myButton = False

End Sub

 

    1. The worker routine is called MyFormat, here it is called from the overlay button …

' The Overlay button

Private Sub btnOverlay_Click()

   Call MyFormat(myOverlay)

End Sub

 

    1. … here it is called from the separate button (the myButton should make sure that the TimeRangeChange event does not interfere with us here) …

' The Separate button

Private Sub btnSeparate_Click()

   myButton = True

   Call MyFormat(mySeparate)

   myButton = False

End Sub

 

    1. … and finally from the zoom event!

' The ZOOM event

Private Sub XYPlotReal_Zoom()

   Call MyFormat(myOverlay)

End Sub

 

    1. Add the TimeRangeChange event handler to support the revert button, but make sure nothing happens if the seperate button is hit:

' The TimeRangeChange event

Private Sub XYPlotReal_TimeRangeChange(ByVal StartTime As String, ByVal EndTime As String)

    If myButton = False Then

       Call MyFormat(myOverlay)

    End If

End Sub

 

    1. Here is the code for MyFormat:

' My formatting routine:

'    opt: myOverlay to overlay the XY Plots

'         mySeparate to separate the XY Plots

Private Sub MyFormat(opt As Integer)


    ' First the position.

    If opt = mySeparate Then

       ' Separate:

        XYPlotChar.Height = 700

        XYPlotReal.Height = 700

        XYPlotChar.Width = 1000

        XYPlotReal.Width = 1000

        XYPlotChar.Left = -14950

        XYPlotReal.Left = -13900

        XYPlotChar.Top = 14950

        XYPlotReal.Top = 14950

    Else

        ' Overlayed:

        XYPlotChar.Height = XYPlotReal.Height

        XYPlotChar.Width = XYPlotReal.Width

        XYPlotChar.Left = XYPlotReal.Left

        XYPlotChar.Top = XYPlotReal.Top

    End If


    ' Now let us define the XY definition ...

    Dim myXYCharDefinition As XYDefinition

    Dim myXYRealDefinition As XYDefinition


    ' ... and get it.

    Set myXYCharDefinition = XYPlotChar.GetDefinition

    Set myXYRealDefinition = XYPlotReal.GetDefinition


    ' We need to loop through the tags, so get an integer

    Dim myTag As Integer


    ' Set the Definition.

    If opt = mySeparate Then

        ' For the separate plots.


        For myTag = 1 To 2

           ' Adjust the formating of the scale ...

           myXYRealDefinition.Tags.Item(myTag).ScaleFormat = _

                 myXYCharDefinition.Tags.Item(myTag).ScaleFormat

           ' ... and set the charactersitic curve ...

           myXYCharDefinition.Tags.Item(myTag).SetScaleConfiguration _

                 False, _

                 pbScaleAutorange, _

                 0, _

                 pbScaleAutorange, _

                 1

           ' ... and the data identically to AutoRange.

           myXYRealDefinition.Tags.Item(myTag).SetScaleConfiguration _

                 False, _

                 pbScaleAutorange, _

                 0, _

                 pbScaleAutorange, _

                 1

        Next myTag

        myXYCharDefinition.XYTitle = "Characteristic Curve"


        ' Apply the definition

        XYPlotChar.SetDefinition myXYCharDefinition

        XYPlotReal.SetDefinition myXYRealDefinition

    Else

        ' For the overlay plots.


        For myTag = 1 To 2

           ' We have to keep the scales in sync,

           ' so we need to remember them somewhere.

           Dim myMin As Double

           Dim myMax As Double

           Dim MaxScale As pbMinMaxEnum

           Dim MinScale As pbMinMaxEnum

           ' Get the type ...

           MaxScale = myXYRealDefinition.Tags.Item(myTag).Max

           MinScale = myXYRealDefinition.Tags.Item(myTag).Min

           ' ... and set the variables accordingly for the maximum ...

           Select Case MaxScale

              Case pbScaleAutorange

                 myMax = myXYRealDefinition.Tags.Item(myTag).AutorangeMax

              Case pbScaleAbsolute

                 myMax = myXYRealDefinition.Tags.Item(myTag).AbsoluteMax

              Case pbScaleDatabase

                 MsgBox "ERROR!"

           End Select

           ' ... and the minimum.


           Select Case MinScale

              Case pbScaleAutorange

                 myMin = myXYRealDefinition.Tags.Item(myTag).AutorangeMin

              Case pbScaleAbsolute

                 myMin = myXYRealDefinition.Tags.Item(myTag).AbsoluteMin

              Case pbScaleDatabase

                 MsgBox "ERROR!"

           End Select

           ' Now set the values ...

           myXYCharDefinition.Tags.Item(myTag).SetScaleConfiguration _

               False, _

               pbScaleAbsolute, _

               myMin, _

               pbScaleAbsolute, _

               myMax

       Next myTag

        myXYCharDefinition.XYTitle = ""


       ' ... and apply them

        XYPlotChar.SetDefinition myXYCharDefinition

    End If

  

    'After we adjusted the scales, now let us take a look at the formatting.

    Dim myXYCharFormatAs XYFormat

    Dim myXYRealFormatAs XYFormat

 

    'Get the format

    Set myXYCharFormat = XYPlotChar.GetFormat

    Set myXYRealFormat = XYPlotReal.GetFormat

 

    ' The characteristic curve

    With myXYCharFormat

        .DisplayScaleInside = True

        .ShowCoefficient = False

        .ShowDescription = False

        .ShowEngUnits = False

        .ShowGrids = myXYRealFormat.ShowGrids

        .ShowLinearCorrelation = myXYRealFormat.ShowLinearCorrelation

        .ShowTagName = False

        .ShowValue = False

        .ShowTitle = myXYRealFormat.ShowTitle

        If opt = mySeparate Then

           .Elements.Item(pbXYPen1).Color = vbBlue

        Else

           .Elements.Item(pbXYPen1).Color = vbRed

        End If

    End With

    XYPlotChar.SetFormat myXYCharFormat

 

    ' The real plot

    Set myXYRealFormat = XYPlotReal.GetFormat

    With myXYRealFormat

        .DisplayScaleInside = True

        .ShowCoefficient = False

        .ShowDescription = False

        .ShowEngUnits = False

        .ShowTagName = False

        .ShowValue = False

        .Elements.Item(pbXYPen1).Color = vbBlue

    End With

    XYPlotReal.SetFormat myXYRealFormat

  

    ' Just a white background ...

    XYPlotChar.BackgroundColor = vbWhite

    If opt = mySeparate Then

        XYPlotReal.BackgroundColor = vbWhite

    Else

        XYPlotReal.BackgroundColor = vbNone

    End If

  

End Sub

Now we have a characteristic curve from an ODBC data source ...

 

XYPic11.jpg

 

... that properly reacts on zoom and time range change!

 

XYPic12.jpg

 

Files MS Access Database & PDI are attached to this post

A new version of the A Few PI ProcessBook VBA Tips white paper has been posted on the library: vCampus Library>White Papers and Tutorials>PI ProcessBook>White Paper - A Few PI ProcessBook VBA Tips.

 

We have added some code examples for Module Relative Displays (MDR) and Element Relative Displays (ERD).

Hello, This is your PI System calling to inform you…

Wouldn’t it be cool to get a call from PI Notifications?

 

Phone calls convey a much higher sense of urgency, and you don’t have to worry about it they were read in time or not! Modern IP telephony provides endless possibilities to relate information in a more timely manner.
PI Notifications comes armed with the perfect delivery channel: web services! Around here, we use Skype(tm) as a convenient and cost effective communication tool, but it could be other tools as well. As an example, let us go ahead and write a little web service that takes a message from PI Notifications and delivers it via Skype.

  1. The first thing to do is create a web service project, so open Microsoft Visual Studio 2008, select File>New>Project>Visual C#>ASP.NET Web Service Application. I decided to call mine MySkypeWebService and I have chosen to write it in C#.
  2. VisualStudio.jpg

  3. Now, we want to call a contact, so we have to create a WAV file and deliver this via Skype. That means we have to add some references to our project:
    1. A .NET reference to PresentationCore and to WindowsBase (MediaPlayer requires this and MediaPlayer is an easy way to get the length of the WAV file).
    2. A .NET reference to System.Speech.Synthesis to make our WAV file.
    3. A COM reference to SKYPE4COMLib for Skype.
      (you first need to download and install the Skype4COM API Wrapper from the Skype Developer Downloads page).

  4. To improve readability of the code, add some “using” statements for the System.Windows.Media, the System.Speech.Synthesis and the System.Speech.AudioFormat namespaces.

  5. Note 1: Making a WAV file and placing a call takes time. For file I/O and other time consuming operations it is recommended to create asynchronous web services. There are various resources available describing how to create an asynchronous Web Service and provide all the necessary information.

  6. So what about the code? Here is how we start with the declaration of the web service:

    using System;

     

    using System.Collections.Generic;

     

    using System.Linq;

     

    using System.Web;

     

    using System.Web.Services;

     

     

     

    // Add .NET Reference to PresentationCore for Media Player

     

    // Add .NET Reference to WindowsBase for Media Player

     

    // Add COM Reference to SKYPE4COMLib for Skype

     

    // Add .NET Reference to System.Speech for the speech engine

     

    using System.Windows.Media;

     

    using System.Speech.Synthesis;

     

    using System.Speech.AudioFormat;

     

     

     

    namespace MySkypeWebService

     

    {

     

        /// <summary>

     

        /// Summary description for Service1

     

        /// </summary>

     

        [WebService(Namespace = "http://tempuri.org/")]

     

        [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]

     

        [System.ComponentModel.ToolboxItem(false)]

     

        // To allow this Web Service to be called from script,

     

        // using ASP.NET AJAX, uncomment the following line.

     

        // [System.Web.Script.Services.ScriptService]

     

        publicclassService1 : System.Web.Services.WebService

     

        {

     

        }

     

    }


  7. Now let us start with the WAV File. The following code consists of a method that will create a WAV file in the appropriate format for Skype to understand. As parameter I have chosen the text and the filename:

    // Creates the WAV from Text

     

    privatevoid Text2WAV(string strOutput, string strFilename)

     

    {

     

        try

     

        {

     

            // The SpeechSynthesizer

     

            SpeechSynthesizer _synth

     

                = newSpeechSynthesizer();

     

            DateTime _time = DateTime.Now;

     

            // Set the format to something SKYPE likes

     

            // 16KHz, 16bit, mono

     

            SpeechAudioFormatInfo _format

     

                = newSpeechAudioFormatInfo(16000,

     

                                            AudioBitsPerSample.Sixteen,

     

                                            AudioChannel.Mono);

     

            _synth.SetOutputToWaveFile(strFilename, _format);

     

            // Slow down a bit, the voice is too fast for me

     

            _synth.Rate = -2;

     

            // Speak!

     

            _synth.Speak(strOutput);

     

            _synth.SetOutputToNull();

     

        }

     

        catch (Exception _ex)

     

        {

     

            System.IO.StreamWriter _StreamWriter

     

                = new System.IO.StreamWriter

     

                    (System.Web.Hosting.HostingEnvironment.MapPath

     

                     ("\\MyWebService_EXCEPTION.TXT"),

     

                     true);

     

     

     

            DateTime _time = DateTime.Now;

     

            _StreamWriter.WriteLine("<!---------------------------------------->");

     

            _StreamWriter.WriteLine("<!-- " + _time.ToLocalTime().ToString() + " -->");

     

            _StreamWriter.WriteLine("<!---------------------------------------->");

     

     

     

            _StreamWriter.WriteLine("<!---------------------------------------->");

     

            _StreamWriter.WriteLine(_ex.ToString());

     

            _StreamWriter.WriteLine("<!---------------------------------------->");

     

     

     

            _StreamWriter.Close();

     

        }

     

    }


  8. Now that we have the WAV file, we will create another method that performs the Skype call. For this we need to know the filename and the Skype contact:

    privatevoid MakeSkypeCall(string strFilename, string strContact)

     

    {

     

        // Need to allow ASP.NET to "run as" system

     

        // and enable IIS to interact with Desktop

     

        try

     

        {

     

            // A workaround to get the length of the audio file

     

            // is to use MediaPlayer

     

            MediaPlayer _mplayer = newMediaPlayer();

     

            Uri _path = newUri(strFilename);

     

            _mplayer.Open(_path);

     

            while (_mplayer.DownloadProgress < 1.0)

     

            {

     

                // Need to wait until everything is settled,

     

                // before to ask for the duration of the file

     

                System.Threading.Thread.Sleep(100);

     

            }

     

            System.Threading.Thread.Sleep(1000);

     

            System.Windows.Duration _duration = _mplayer.NaturalDuration;

     

            _mplayer.Close();

     

     

     

            // Get the Skype Object

     

            SKYPE4COMLib.Skype _oSkype

     

                = new SKYPE4COMLib.Skype();

     

            // Search for the user in the contacts

     

            SKYPE4COMLib.User _oUser

     

                = _oSkype.SearchForUsers(strContact)[1];

     

            // Create the SkypeCall

     

            SKYPE4COMLib.Call _oCall

     

                = new SKYPE4COMLib.Call();

     

            _oCall = _oSkype.PlaceCall(strContact, "", "", "");

     

            // Wait until the call is picked up or 180 seconds

     

            int _i = 0;

     

            while ((_oCall.Status !=

     

                        _oSkype.Convert.TextToCallStatus("INPROGRESS"))

     

                    & (_i < 1800))

     

            {

     

                _i++;

     

                System.Threading.Thread.Sleep(100);

     

            }

     

            if (_i < 1800)

     

            {

     

                System.Threading.Thread.Sleep(1000);

     

                // Play the wav

     

                _oCall.set_InputDevice

     

                    (SKYPE4COMLib.TCallIoDeviceType.callIoDeviceTypeFile,

     

                     strFilename);

     

                // Wait for the WAV file to be finish playing before to hang up

     

                System.Threading.Thread.Sleep

     

                    (Convert.ToInt32(_duration.TimeSpan.TotalMilliseconds) + 500);

     

                // Hang up

     

                _oCall.Finish();

     

            }

     

            else

     

            {

     

                System.IO.StreamWriter _StreamWriter;

     

                _StreamWriter = new System.IO.StreamWriter

     

                    (System.Web.Hosting.HostingEnvironment.MapPath

     

                        ("\\MyWebService_NoAnswer.TXT"),

     

                     true);

     

     

     

                DateTime _time = DateTime.Now;

     

                _StreamWriter.WriteLine("<!---------------------------------------->");

     

                _StreamWriter.WriteLine("<!-- " + _time.ToLocalTime().ToString() + " -->");

     

                _StreamWriter.WriteLine("<!---------------------------------------->");

     

     

     

                _StreamWriter.WriteLine("<!---------------------------------------->");

     

                _StreamWriter.WriteLine(strFilename);

     

                _StreamWriter.WriteLine("<!---------------------------------------->");

     

     

     

                _StreamWriter.Close();

     

            }

     

        }

     

        catch (Exception _ex)

     

        {

     

            System.IO.StreamWriter _StreamWriter;

     

            _StreamWriter = new System.IO.StreamWriter

     

                (System.Web.Hosting.HostingEnvironment.MapPath

     

                    ("\\MyWebService_EXCEPTION.TXT"),

     

                 true);

     

     

     

            DateTime _time = DateTime.Now;

     

            _StreamWriter.WriteLine("<!---------------------------------------->");

     

            _StreamWriter.WriteLine("<!-- " + _time.ToLocalTime().ToString() + " -->");

     

            _StreamWriter.WriteLine("<!---------------------------------------->");

     

     

     

            _StreamWriter.WriteLine("<!---------------------------------------->");

     

            _StreamWriter.WriteLine(_ex.ToString());

     

            _StreamWriter.WriteLine("<!---------------------------------------->");

     

     

     

            _StreamWriter.Close();

     

        }

     

    }


  9. So let us recall here: we need something to send from PI Notifications to this web service. I have chosen to take four parameters: the Element (in fact I will use the Notification instead of the Element as it is more user-friendly than an element including path), the trigger time, a description and finally the Skype contact. The following code consists of a method that creates my WAV file (we can keep it for further documentation), creates a reasonable message from the element, the trigger time and the description, and calls the code to create my WAV file and do the Skype call.

    publicstring Wav2Skype(string strElement,

     

                            string strTime,

     

                            string strDescription,

     

                            string strContact)

     

    {

     

        DateTime _time = DateTime.Now;

     

        string strFilename;

     

        strFilename = System.Web.Hosting.HostingEnvironment.MapPath

     

                        ("\\Note_"

     

                         + _time.ToString("dd_MM_yy_HH_mm_ss")

     

                         + ".wav");

     

        Text2WAV(strElement

     

                 + ", " + strTime

     

                 + ", " + strDescription,

     

                 strFilename);

     

        MakeSkypeCall(strFilename, strContact);

     

        return"DONE";

     

    }


  10. Looks like all the heavy-lifting is done! All we need now is we need to expose a web service. Based on the information from MSDN here, we need a delegate and two web methods as below:

    // Delegate

     

    publicdelegatestringWav2SkypeAsyncStub(string strElement,

     

                                              string strTime,

     

                                              string strDescription,

     

                                              string strContact);

     

     

     

    // Actual method which is exposed as a web service

     

    [WebMethod]

     

    publicIAsyncResult BeginWav2Skype(string strElement,

     

                                       string strTime,

     

                                       string strDescription,

     

                                       string strContact,

     

                                       AsyncCallback asyncCallback,

     

                                       object asyncState)

     

    {

     

        Wav2SkypeAsyncStub _asyncStub

     

            = newWav2SkypeAsyncStub(Wav2Skype);

     

        // Using delegate for asynchronous implementation  

     

        return _asyncStub.BeginInvoke(strElement,

     

                                      strTime,

     

                                      strDescription,

     

                                      strContact,

     

                                      asyncCallback,

     

                                      null);

     

    } // BeginWav2Skype

     

     

     

    [WebMethod]

     

    publicstring EndWav2Skype(IAsyncResult result)

     

    {

     

        return"OK";

     

    } // EndWav2Skype


  11. Build MySkypeWebService using the Build>Build MySkypeWebService menu in Visual Studio.


  12. At this point our web service would now work - well almost. Skype is a nice tool, but keep in mind it is an interactive desktop application which leads to two issues:
    1. We have to open Skype and log in (or set Skype to automatically start and log in at Windows startup).
    2. By default neither IIS nor ASP.NET can interact with the desktop of a user.

    The first issue cannot be solved: you have to be logged in into Skype on the machine providing the web service. The second issue can be solved by doing the following:
    1. In the Services applet, open the properties for the IIS Admin Service service and check the Allow service to interact with desktop option on the Log On tab.
    2. IIS.jpg

    3. Edit the following file
      C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\machine.config
      by locating the line
      <processModel />
      and changing it to
      <processModel autoConfig="true" userName="SYSTEM" password="AutoGenerate"/>


    Note 2: If you want to test your web service with the Echo / Sound Test Service (echo123) you will not hear any echo in your speakers while playing the WAV file.
    Note 3: You will have to set a break point in the web service to not start playing before you are instructed to speak by the Echo / Sound Test Service.

  13. Publish the MySkypeWebService using the Build>Publish MySkypeWebService menu in Visual Studio.


  14. Now we should be free to use the web service in PI Notifications:

    Notifications.jpg

  15. And receive a call from PI Notifications:
  16. ASCalling.jpg


Challenge for the community

  1. The above solution requires someone to be logged in to run Skype. I did not find a way to programmatically log in to Skype to avoid this.
  2. We have the wav file. We deliver it via Skype - how about a voice modem as another option?
andreas

PDI Preview Handler

Posted by andreas Employee Jan 14, 2010

Preview Handler

Why?

The Preview Handlers have been introduced with Microsoft Windows Vista and with Microsoft Office 2007.
However, if you try to preview an attached PDI in Microsoft Outlook, you will see something similar to that:

 

NoPreviewHandler.jpg

 

When getting Windows 7 on my Notebook recently, Steve gave me a head's up to try and write a preview handler for PDI's.

How?

A good start are this article of Stephen Toub, Managed Preview Handlers for Vista and Office in Daniel Moth's blog, More Preview Handlers from the same Blog and especially the screencast by Daniel Moth on channel9.

Requirements

So what do you need (beside taking a look at the above resources)?

  1. Microsoft Windows Vista or Microsoft Windows 7
  2. Microsoft Visual Studio 2008
  3. The Managed Preview Handler Framework from here.
  4. OSIsoft PI ProcessBook 3.x or newer
  5. Microsoft Outlook 2007

The Code

And here is my Hello PI preview handler ;-)

  1. Download the msdn magazine preview handler framework from the first link.
  2. To build MsdnMagPreviewHandlers.dll, you might have as well to install the Microsoft Visual J# Redistributable Package
  3. As in Daniel Moth's example, create a new class library.
  4. Reference the MsdnMagPreviewHandlers.dll.
  5. Add a reference to System.Windows.Forms.
  6. Sign the assembly with a strong name key file.
  7. To render a PDI file we will need an ActiveX control. Hence we are here in .NET we need to create a managed wrapper for the PI ProcessBook Display Control. So we are going to create that wrapper with AxImp.exe:

  8. AxImp.exe

     

       /keyfile:.\OSIsoftvCampusPreviewHandler\OSIsoftvCampusPreviewHandler.snk

     

       c:\PIPC\Procbook\pbdctrl.ocx


  9. We will have to add a reference to our AxPBDCtrl.dll.
  10. The code for the preview handler is below:

  11. using System;

     

    using System.Collections.Generic;

     

    using System.Linq;

     

    using System.Text;

     

    using MsdnMag;

     

    using System.Runtime.InteropServices;

     

    using System.IO;

     

    using System.Windows.Forms;

     

     

     

    namespace OSIsoftvCampusPreviewHandler

     

    {

     

        [PreviewHandler("OSIsoft vCampus Preview handler",

     

                        ".pdi",

     

                        "{1A68DC08-3E1F-4f6a-B5B0-CF7C8D6FC5CB}")]

     

        [ProgId("OSIsoft.OSIsoftvCampusPDIPreviewHandler")]

     

        [Guid("22D50838-F245-4e3a-8371-75FF1D2B590D")]

     

        [ClassInterface(ClassInterfaceType.None)]

     

        [ComVisible(true)]

     

        publicsealedclassOSIsoftvCampusPDIPreviewHandler : FileBasedPreviewHandler

     

        {

     

            protectedoverridePreviewHandlerControl CreatePreviewHandlerControl()

     

            {

     

                returnnewOSIsoftvCampusPDIPreviewHandlerControl();

     

            }

     

     

     

            privatesealedclassOSIsoftvCampusPDIPreviewHandlerControl :

     

                                 FileBasedPreviewHandlerControl

     

            {

     

                publicoverridevoid Load(FileInfo file)

     

                {

     

                    AxHost _pbDisplay = new AxPBDCtrl.AxPbd();

     

                    Controls.Add(_pbDisplay);

     

                    IntPtr forceCreation = _pbDisplay.Handle;

     

                    _pbDisplay.Dock = DockStyle.Fill;

     

     

     

                    AxPBDCtrl.AxPbd _AxPbd = (AxPBDCtrl.AxPbd)_pbDisplay;

     

                    _AxPbd.DisplayURL = file.ToString();

     

                }

     

            }

     

        }

     

    }


  12. After building the dll, we have to move it to the Global Assembly Cache (GAC) and register it:

  13. GACUTIL.EXE -i MsdnMagPreviewHandlers.dll

     

    GACUTIL.EXE -i OSIsoftvCampusPreviewHandler.dll

     

    GACUTIL.EXE -i AxPBDCtrl.dll

     

    GACUTIL.EXE -i PBDCtrl.dll

     

    REGASM.EXE /codebase MsdnMagPreviewHandlers.dll

     

    REGASM.EXE /codebase OSIsoftvCampusPreviewHandler.dll

     

    REGASM.EXE /codebase AxPBDCtrl.dll

     

    REGASM.EXE /codebase PBDCtrl.dll


The result is a PI ProcessBook Display preview handler that allows you to preview a PDI in Outlook

 

WithPreviewHandler.jpg

 

or Windows Explorer

 

WithPreviewHandlerExplorer.jpg

 
Notes:
  1. In this example we are using PI ProcessBook. However, all of this is possible with PI ActiveView as well (Note that PI ActiveView is not part of the OSIsoft vCampus PI Products Kit).
    PI ActiveView provides a means to view and interact with PI ProcessBook displays outside of PI ProcessBook. By embedding the PI ActiveView ActiveX control in other applications, such as Internet Explorer, and installing the local executable, users can view PI ProcessBook PDI display files without modification.
  2. This PreviewHandler uses the PI ProcessBook Display control to preview a PDI. Therefore it has the same requirements as PI ProcessBook: you need to have a PI SDK connection to the PI Server and the necessary access rights to view the data.
  3. The two batch files to register and unregister as well as the source code above can be downloaded here.

Call for Help

The community is explicitly invited to improve/extend my code. As an example - a known limitation is that the preview handler does not scale the display according to the visible area in the preview.

The last posts on my Blog did show you some small bits of code I think are useful. However, I recently discovered this almost ready white paper floating around on my desktop. After I have finished it (Thanks to the original contributors for some of the included material!) I thought it is never going to be "finished" at all.

 

So I would like to invite you to take a look at the "A Few ProcessBook VBA Tips" white paper on the vCampus Library (under the "White Papers and Tutorials>PI ProcessBook" branch) and let me know what you think of it as a "working document" - I would like to ask you if I should continue to collect little code snippets, write my own, etc. and add this to the White Paper to have one place to look for ideas and VBA sample code?

Filter Blog

By date: By tag: