Creating an ARDI Live Driver

This tutorial will re-create the OSI PI driver as an example of how to create your own ARDI driver in .NET

The Sketch

To begin, create a new console project in Visual Studio.

In your Program.cs, you need to define two classes.

program.cs
//ARDI Driver SDK
using ARDIDriverFramework;
 
//PI .NET SDK
using OSIsoft.AF; 
using OSIsoft.AF.Asset;
 
namespace PILiveDriver
{
    class Program
    {
        class PILiveDriver : ARDILiveDriver
        {
            public override ARDICore CreateCore()
            {
                return new PILiveConnection();
            }
        }
 
        class PILiveConnection : ARDILiveDriverCore
        {
            public override bool Connect()
            {
               return false;
            }
 
            public override bool Disconnect()
            {
               return false;
            }
 
            public override bool Poll()
            {
               return false;
            }
        }
 
        static void Main(string[] args)
        {
            PILiveDriver Driver = new PILiveDriver();
            Driver.Run(args);           
        }
    }
}

This is the basic layout of every ARDI .NET driver.

ARDILiveData

Your class has access to some important functions and variables.

NewData(string Key, string Value)

This function is responsible for queuing live data to be sent to ARDI (the data isn't physically sent until the poll function completes).

ParameterPurpose
KeyThe address of the data point that has been read
ValueThe value read from your data source

Log(int Level, string Message)

Logs information back to the debug log for the driver.

Dictionary<string, List<PointInfo>> Addresses

This member contains a list of all of the data points the driver needs to load.

Each point contains the following information…

MemberDescription
addressThe address (or lookup value) for the point.
codeThe ARDI code for this point, in asset id:property id:attribute format.

While there are other attributes of the point, these are used internally and you should not need to access them.

Functions

There are five key functions required in your driver class.

Connect

The connect function is called whenever your driver is to connect to a data source. It is passed the driver address (which is often a colon-delimited string).

It returns True if connection was successful and False if connection fails.

Disconnect

This does the opposite of connect - it closes the connection to your data source and does any cleanup required.

Optimise

The Optimise function is called when the driver has successfully connected, but before the first call to poll.

The function is also called whenever a change occurs to the data links in ARDI that would effect your driver (ie. when a new asset is linked to data from your source).

The code in this function should perform any work that needs to be done to streamline the work of the poll function in getting data from the source.

For example, if this was an SQL data source, it would probably create your SQL statement here.

Poll

The poll function is called in a constant loop every time ARDI would like additional data from the data source (the rate this occurs is the refresh rate of the driver configured in the ARDI web interface).

In this function you perform the actual request for new data (or if your data requests are asynchronous, deliver your most recent data).

Implementation

Connect

First, we need do read in the address we are supposed to be connecting to.

The address for our text file driver will include the following options…

OptionDescription
ServerThe name of the PI AF Server
DatabaseThe name of the specific AF database
UsernameOptional - the username to login as
PasswordOptional - the password to login with

The address lists these options, separated by a colon (:) character. IE. \\MYPISERVER:MyAssets::.

The first steps of the Connect function are to…

  • Split the address string into its component parts
  • Record this information to internal members
        string[] pieces = Address.Split(':');
        string Server = "";
        string DBName = "";
        string Username = "";
        string Password = "";
 
        if (pieces.Length < 4) return false;
 
        Server = pieces[0];
        DBName = pieces[1];
        Username = pieces[2];
        Password = pieces[3];
 

Connecting

Next, we establish a connection with the data source. In this case, we'd like to need to know a few things….

  • How we are connected to PI (ConnectedSystem)
  • A link to the specific PI database we want to work with (DB)
  • A list of 'AF Attributes' (individual data points) we want to keep an eye on (PollingSet)
   //Get a list of all of the available PI systems.
 
   Log(1, "Connecting to PI Server");
   PISystems SystemList = null;
   try
   {
       SystemList = new PISystems();
   }
   catch(Exception e)
   {
       Log(0,"Couldn't get list of PI systems - " + e.Message);
       return false;
   }
 
   //Find the server we have requested.
 
   try
   {
         ConnectedSystem = SystemList[Server];
         if (Username != "")
         {
                        Log(0, "Authenticating With PI system for user " + Username);
                        ConnectedSystem.Connect(new NetworkCredential(Username, Password));
         }
    }
    catch(Exception e)
    {
                    Log(0, "Couldn't find named PI system '" + pieces[0] + "' " + e.Message);
                    return false;
    }
 
    //Exit if we couldn't get one.
   if(ConnectedSystem == null)
    {
                    Log(0, "PI System '" + Server + "' not found.");
                    return false;
    }
 
    //Grab a reference to the database itself.
 
    try
    {
                    DB = ConnectedSystem.Databases[DBName];
    }
    catch
    {
                    Log(0, "PI Database'" + DBName + "' not found.");
                    return false;
     }
 

Finally, we need to get a list of the attributes we are interested in monitoring. To do this, we loop through each of our addresses, clean up the names slightly (in case the user used the incorrect backslash character), and ask PI for the matching attribute.

    foreach (KeyValuePair<string, List<PointInfo>> KV in Addresses)
    {
                    FullPath = KV.Key.Replace('/', '\\');
                    try
                    {
                        AFAttribute Attr = new AFAttribute(DB, FullPath);                        
                        PollingSet.Add(Attr);
                    }
                    catch
                    {
                        Log(1, "Unable to located PI AF address '" + FullPath + "'");
                    }
                    continue;
     }

Polling

Now that we have a list of the points we want to watch, we can fill in our poll function.

Poll is repeatedly called to ensure that the live values we are working with are running as expected.

If the data we fetch is bad, return the “^” symbol (which ARDI interprets as 'Bad Data').

   //Fetch a list of fresh values from PI Asset Framework
   AFValues valueset = PollingSet.GetValue();
 
   foreach(AFValue V in valueset)
   {
                    //Figure out the value for this point.
                    if (!V.IsGood)
                    {
                        val = "^";                                               
                    {
                    else
                    {
                        try
                        {
                            Type Tx = V.ValueType;
                            try
                            {
                                if (Tx.Name == "AFEnumerationValue")
                                {
                                    val = V.ValueAsInt32().ToString();
                                }                                
                            }
                            catch
                            {
                                val = V.Value.ToString();
                            }
                            if (val == "") val = V.Value.ToString();
                        }
                        catch(Exception e)
                        {
                            val = "^";                            
                        }
                    }
 
                    //Write the data out (if required), based on the PI address.
                    NewData(V.Attribute.GetPath(), val);
    }

Creating a Web Interface

The last step is to create a web interface for our driver, so that we can set the drivers various options (file name, delimiter, column numbers etc.) in a friendly manner.

This will require some basic HTML & PHP.

Firstly, copy one of the existing drivers user interfaces by copying /opt/ardi/web/data/live/pi to /opt/ardi/web/data/live/pi (the folder name must match the name of the driver folder & file).

There are several PHP files in this folder. Click on them for examples and documentation on what they do.

FilePurpose
info.incProvides details on the driver
configure-source.phpThe user interface for setting up the data source
saveconfig-source.phpConvert the properties set in configure-source.php to a machine-readable address string
friendlyname.phpConvert the address from saveconfig-source.php to a human-readable text string
link.phpThe user interface for setting up a data link between the source and an asset property
encode.phpConvert the properties set in link.php to a machine-readable address string
decode.phpConvert the address from encode.php to a human-readable description