Skip navigation
All Places > PI Developers Club > Blog > Authors dng

PI Developers Club

9 Posts authored by: dng Employee
dng

Raspberry Pi to the PI System

Posted by dng Employee Nov 12, 2015

Yesterday, Barry posted a blog post about writing to the PI System using the Arduino Yun. I figured I’ll join in this Internet of Things fun by posting my experience on writing to PI using Raspberry Pi.

 

 

Setup

Similar to Barry’s set up, I am collecting temperature data via a temperature sensor. The temperature data is then sent to the PI System by posting to a public PI Web API endpoint. Instead of Arduino Yun, we are using Raspberry Pi here.

 

Here is the setup:

rpi_setup.JPG

 

  • Sensor: DS18B20 digital temperature sensor
    • The sensor was set up according to this tutorial with the help of James Dryden (thanks James!)
  • Development system: Raspberry Pi
    • I am using an older model of Raspberry Pi (Model B) running Raspbian.
  • Gateway device: Digi TransPort WR21 wireless router
    • This is a cellular router, allowing us to send data to the PI server via the cellular LTE network. This potentially allow us to install our sensor at any location as long as cellular network is available!
  • PI System: PI Web API with PI Data Archive 2015.
    • The PI Data Archive, as well as the PI Web API instance, is hosted on Azure. The PI Web API endpoint is publicly exposed.

 

 

Python Script

Once everything is set up, I used a simple Python script to write the temperature data to PI every 5 seconds.

 

WriteTemp.py (GitHub gist here):

 

import requests
import time
import os
import glob
import re

base_url = 'https://<PI Web API server>/piwebapi'
webId = '<WebId to PI Tag>'
base_dir = '/sys/bus/w1/devices/'
yes_pattern = re.compile(r'YES')
temp_pattern = re.compile('t=(\d+)')

def initialize_sensor():
    os.system('sudo modprobe w1-gpio')
    os.system('sudo modprobe w1-therm')

def get_sensor_file():
    device_folder = glob.glob(base_dir + '28-*')[0]
    return device_folder + '/w1_slave'

def read_temp_raw(file):
    f = open(device_file,'r')
    lines = f.read()
    f.close()
    return lines

def read_temp(file):
    lines = read_temp_raw(file)
    while not yes_pattern.search(lines):
        time.sleep(0.2)
        lines = read_temp_raw(file)
    temp_string = temp_pattern.search(lines).group(1)
    temp_c = float(temp_string) / 1000.0
    temp_f = temp_c * 9.0 / 5.0 + 32.0
    return temp_f

def post_pi_value(value):
    data = {'Value': value}
    headers = {'Content-Type': 'application/json'}
    response = requests.post(base_url + '/streams/' + webId + '/value', json=data, headers=headers, verify=False)
    return response

if __name__ == '__main__':
    initialize_sensor()
    device_file = get_sensor_file()
    while True:
        value = read_temp(device_file)
        response = post_pi_value(value)
        print(response)
        time.sleep(5)

 

In this script, I’m posting the temperature value to the PI Web API Stream controller > UpdateValue action every 5 seconds (timestamp is current time by default). The post_pi_value function is all we need to write to PI Web API! For more information on how to work with HTTP requests in Python, check out the Python library Requests. Regarding the code on on how to get temperature reading from the sensor, check out the tutorial for the DS18B20 temperature sensor.

 

 

Running on startup

Finally, I set this Python script to run on startup by editing the rc.local file at /etc/rc.local by including this line:

 

sudo python3 /path/to/WriteTemp.py > /dev/null 2>&1 &

 

 

Results

Running the script (which occurs automatically after rebooting the Raspberry Pi), we can see live temperature readings in the PI Data Archive!

 

On a normal day in the office:

 

On a particular warm day in the office:

 

We can clearly see the temperature going up at the beginning of business day! (heater kicking off? when people starting to come into the office?)

 

Conclusion

Through these simple set ups, we hope to show you the possibility of integrating the PI System with these IoT devices. Let us know your experience with IoT + PI!

The PI Web API 2015 R3 is released! There are many exciting new features that come with this release, including the much discussed batch CTP and channel CTP. The purpose of this blog post is to test out some of the new features, as well as write a simple JavaScript application that incorporate batch and channel to see how they work.

 

Enhancements

Let’s take a look at some of the enhancements in this release (see release notes here).

 

Search AF Elements or Event Frames by their attribute value

It looks like this functionality is enabled in the actions CreateSearchByAttribute and ExecuteSearchByAttribute in the Element and EventFrame controllers. Let’s test this out in a REST client.

 

I’m using the NuGreen database:

 

In this database, Houston, Little Rock, Tucson, and Wichita belongs to the element template “Plant”. I would like to search for all element where the Environment attribute is above 0.

 

Using the CreateSearchByAttribute action in the Element Controller:

Sending HTTP POST request to https://MyPIWebAPIServer/piwebapi/elements/searchbyattribute with payload:

 

{
    "SearchRoot":"E0G1eh97OAw0KXIEzJrhPzTAzVOQ_L9z4xGT8wAVXYKvGgRE5HLUFGMjAxNFxOVUdSRUVOXE5VR1JFRU4",
    "ElementTemplate":"T0G1eh97OAw0KXIEzJrhPzTALcH_J_O6eUyAvrBdmUbkgARE5HLUFGMjAxNFxOVUdSRUVOXEVMRU1FTlRURU1QTEFURVNbUExBTlRd",
    "ValueQueries":
    [
        {
            "AttributeName":"Environment",
            "AttributeValue":"0",
            "SearchOperator":"GreaterThan"
        }
    ]
}

 

SearchRoot contains the WebId of NuGreen, and ElementTemplate contains the WebId of the “Plant” Element Template. It looks like only the root template’s child elements are searched, but not the full hierarchy (e.g. if SearchRoot is not specified, it defaults to the Asset Database; however, this search returns no results because the city elements are not direct children of the root asset database). In the response’s “location” header, I was able to get the link which store the SearchId. The SearchId is simply an encoded string of the query.

 

Using the ExecuteSearchByAttribute action in the Element Controller:

From the previous request, I was able to get the search result in the response content (first page only). To get the response by a separate GET request, I sent a HTTP GET request to https://MyPIWebAPIServer/piwebapi/elements/searchbyattribute/{searchId}(where searchId is obtained from the previous request). The response body contains the resulting elements based on the search criteria!

 

 

Allow any HTTP method request as HTTP POST request

In this release, you can submit a HTTP POST request to the home controller of PI Web API with the appropriate header to reach any resources. E.g.

 

Sending a HTTP POST request to: https://MyPIWebAPIServer/piwebapi with headers:

 

You will receive the same response as submitting a GET request to https://MyPIWebAPIServer/piwebapi/system/versions directly.

 

 

Batch CTP and Channel CTP

Now, onto the much anticipated Batch and Channel CTPs! I have decided to write a JavaScript plotting application to test out these new functionalities. This is going to be an application where:

  • User can enter a tag mask and get a list of tag (batch demo).
  • User can then select a tag, enter a time interval, and plot a trend (normal HTTP request).
  • The plot will automatically update based on new values coming in (channels demo).

 

Environment

I am using Visual Studio 2015 to write this application. Create a new empty ASP.NET Web Application without any template.

 

To start, let’s add the following NuGet packages:

  • Bootstrap (Install-Package bootstrap)
  • jQuery (Install-Package jQuery)
  • Flot (Install-Package flot)

 

Flot is an open-sourced JavaScript plotting library. I picked it based on its simplicity and support for time-series plot. For more information, please refer to the Flot documentation. If you are interested in other plotting options, check out this previous discussion.

 

Let’s also create the files needed to build this application:

  • index.html (under the root): main HTML page
  • app.js (under the Scripts folder): main JavaScript file
  • piwebapi_wrapper.js (under the Scripts folder): PI Web API wrapper.

 

HTML page

Let’s write the HTML page (index.html):

 

<!DOCTYPE html>
<html>
<head>
    <title>Plotting with PI Web API Channels</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" type="text/css" href="Content/bootstrap.min.css" />
</head>
<body>
    <div class="container">
        <!-- User login form -->
        <div id="auth-view-mode">
            <h1>Plotting with PI Web API Channels: Login Page</h1>
            <form>
                <div class="form-group">
                    <label for="username">User name</label>
                    <input type="text" class="form-control" id="username" placeholder="User name" />
                </div>
                <div class="form-group">
                    <label for="password">Password</label>
                    <input type="password" class="form-control" id="password" placeholder="Password" />
                </div>
                <button type="button" id="go-to-plot-btn" class="btn btn-default">Submit</button>
            </form>
        </div>

        <!-- Plotting screen -->
        <div id="plot-view-mode" style="display: none;">
            <h1>Plotting with PI Web API Channels</h1>
            <br />
            <div id="nav">

                <!-- Tag search -->
                <form class="form-inline">
                    <input type="text" class="form-control" id="tagmask-text" placeholder="Tag Mask" />
                    <button type="button" id="search-btn" class="btn btn-default">Search for tags</button>
                </form>
                <br />

                <!-- Information to plot -->
                <form class="form-inline">
                    <select class="form-control" id="tag-select"></select>
                    <div class="form-group">
                        <input type="text" class="form-control" id="time-interval-text" placeholder="Time Interval" />
                        <select class="form-control" id="time-interval-unit">
                            <option value="h">hour(s)</option>
                            <option value="m">minute(s)</option>
                            <option value="s">second(s)</option>
                        </select>
                    </div>
                    <button type="button" id="plot-btn" class="btn btn-primary" disabled>Start Plot</button>
                    <button type="button" id="stop-btn" class="btn btn-danger" disabled>Stop Update</button>
                    <button type="button" id="back-btn" class="btn btn-default">Back to login page</button>
                </form>
                <br />
            </div>

            <!-- Placeholder for plot -->
            <div id="plot" style="width:800px; height:400px"></div>
        </div>
    </div>

    <script type="text/javascript" src="Scripts/jquery-2.1.4.min.js"></script>
    <script type="text/javascript" src="Scripts/flot/jquery.flot.min.js"></script>
    <script type="text/javascript" src="Scripts/flot/jquery.flot.time.min.js"></script>
    <script type="text/javascript" src="Scripts/bootstrap.min.js"></script>
    <script type="text/javascript" src="Scripts/piwebapi_wrapper.js"></script>
    <script type="text/javascript" src="Scripts/app.js"></script>
</body>
</html>


 

You will notice that the web application is divided in two main divs. The first div is the login form that allows user to enter in the user name and password. The second div, initially hidden, is the main page of the plot application. (Credit to Marcos for the original source of the login form.) To see the html page, remove the style="display: none;" in the “plot-view-mode” div.

Don’t forget to put the style attribute back in the div after visualizing the HTML page.

 

Login – Basic Authentication

When user enters the username and password, we would like to make a HTTP GET request to the home controller to check if the credentials are correct. If not, an error alert is displayed.

 

First, let’s add some methods in the PI Web API wrapper (piwebapi_wrapper.js) to send ajax request based on the provided username and password.

var basePIWebAPIUrl = "https://YourPIWebAPIServer/piwebapi";
var piDataArchiveName = "YourPIDataArchive";

// PI Web API wrapper
var piwebapi = (function () {
    var currentUserName = null;
    var currentPassword = null;

    // Send ajax request
    var processJsonContent = function (url, type, data, successCallBack, errorCallBack) {
        return $.ajax({
            url: encodeURI(url),
            type: type,
            data: data,
            contentType: "application/json; charset=UTF-8",
            beforeSend: function (xhr) {
                xhr.setRequestHeader("Authorization", makeBasicAuth(currentUserName, currentPassword));
            },
            success: successCallBack,
            error: errorCallBack
        });
    };

    // Return authorization header value for basic authentication
    var makeBasicAuth = function (user, password) {
        var tok = user + ':' + password;
        var hash = window.btoa(tok);
        return "Basic " + hash;
    };

    // Publicly available methods
    return {
        // Store current username and password
        SetCredentials: function (user, password) {
            currentUserName = user;
            currentPassword = password;
        },

        // Test basic authentication
        Authorize: function (successCallBack, errorCallBack) {
            return processJsonContent(basePIWebAPIUrl, 'GET', null, successCallBack, errorCallBack);
        },

        // Reset username/password and web socket
        Reset: function () {
            currentUserName = null;
            currentPassword = null;
        }
    }
})();

 

Note that the above is written as a JavaScript module. (For more information, please refer to this previous blog post. We have two private functions and three public functions:

  • Private
    • processJsonContent: send an ajax request to the specified URL, supply different callbacks when the request is successful or not.
    • makeBasicAuth: return the authorization header for basic authentication. The authorization header is the base64-encoded string of the username:password.
  • Public
    • SetCredentials: store the username and password.
    • Authorize: submit a GET request to the home controller to test credentials.
    • Reset: reset private variables in the module.

 

In the app.js file:

// Reset buttons, text boxes and plot in the plot view
var resetPlotView = function () {
    $('#tagmask-text').val('');
    $('#tag-select').empty();
    $('#time-interval-text').val('');
    $('#plot-btn').attr('disabled', 'disabled');
    $('#stop-btn').attr('disabled', 'disabled');
    $('#plot').empty();
};

// Test username/password and go to plot screen
$('#go-to-plot-btn').click(function () {
    // Store username and password
    var username = $("#username").val();
    var password = $("#password").val();
    piwebapi.SetCredentials(username, password);

    // Check authentication
    var authSuccessCallBack = function (data, statusMessage, statusObj) {
        if (statusObj.status == 200) {
            $('#auth-view-mode').hide();
            $('#plot-view-mode').show();
        }
    };
    var authErrorCallBack = function (data) {
        if (data.status == 401) {
            alert("Invalid username and password.");
        }
        else {
            alert("Error during validation.");
        }
    };
    piwebapi.Authorize(authSuccessCallBack, authErrorCallBack);
});

// Go back to login screen
$("#back-btn").click(function () {
    $("#username").val('');
    $("#password").val('');

    $("#plot-view-mode").hide();
    $("#auth-view-mode").show();

    piwebapi.Reset();
    resetPlotView();
});


 

Testing the application, we can now test authentication from the login screen by providing the correct pair of username and password. We can also go back to the login page by clicking the “Back to login page” button on the main plotting page. Note that the username and password will be used to make every HTTP request in the application via basic authentication (see processAjaxContent function).

 

Tag Search – Batch Demo

Let’s build the functionality on the “Search for tags” button. In a PI Data Archive, we would like to search the list of tags by specifying a tag mask. (Of course, this can easily be achieved using the indexed search, but let’s take the opportunity to test out the new batch functionality here). Looking at the GetPoints action in the DataServer controller, we can submit a GET request to dataservers/{webId}/points and specify a nameFilter. This will give us a list of all the matching points. As part of the URL, we need to specify the WebId for the PI Data Archive, which can be achieved by using the GetByName action in the DataServer controller. Why don’t we bundle these requests together to test out Batch?!

 

Notes about batch: We can now batch multiple REST requests into a single HTTP request. This could be helpful in scenarios where network speed between the REST client and the PI Web API server is the bottleneck. The help file provides a detailed example of a sample batch request. In the payload, you can specify a list of requests with specific IDs for each. You can also specify dependency by listing the parent IDs in each request (i.e. parent requests must complete before the child request).

 

In the piwebapi_wrapper.js file, let’s add a new private function:

// BATCH demonstration: get list of tags based on particular PI Data Archive and tag mask
var getTagListRequest = function (piDataArchive, tagMask, successCallBack, errorCallBack) {
    var url = basePIWebAPIUrl + "/batch";
    var data = {};
    data["1"] = {
        "Method": "GET",
        "Resource": encodeURI(basePIWebAPIUrl + "/dataservers?name=" + piDataArchive)
    };
    data["2"] = {
        "Method": "GET",
        "Resource": "{0}?nameFilter=" + encodeURIComponent(tagMask),
        "Parameters": [
            "$.1.Content.Links.Points"
        ],
        "ParentIds": [
            "1"
        ]
    };
    return processJsonContent(url, "POST", JSON.stringify(data), successCallBack, errorCallBack);
};

 

and a public function:

 

        // Get the list of tags based on tagmask
        GetTagList: function (tagMask, processFunction, errorFunction, errorCallBack) {
            var successCallBack = function (data) {
                var filteredTags = {};
                if (data["1"].Status == 200 && data["2"].Status == 200) {
                    for (var i = 0; i < data["2"].Content.Items.length; i++) {
                        filteredTags[data["2"].Content.Items[i].Name] = data["2"].Content.Items[i].WebId;
                    }
                    processFunction(filteredTags);
                }
                else {
                    errorFunction();
                }
            };
            getTagListRequest(piDataArchiveName, tagMask, successCallBack, errorCallBack);
        }

 

The getTagListRequest method bundles 2 dependent requests into 1 HTTP request:

  1. First, to get PI Web API endpoints for a specific PI Data Archive
  2. Then, use the Points endpoint to get the list of tags satisfying the provided tag mask.

 

The GetTagList public method calls the private getTagListRequest method and process the results. Since a batch request will return a status code of 207 even if some of the batched requests failed, we need to check individual status code for each request from the response body. Since the response will contain an Id identical to the request Id (1 and 2 in this case), we can easily determine which response correlates with each request.

 

In the app.js file, call the GetTagList function when user clicks on the “Search for tags” button:

 

var piTagWebIds = {}; 

// General error call back
var errorCallBack = function (xhr) {
    console.log(xhr.responseText);
}

// Search for tags using tag mask
$('#search-btn').click(function (data) {
    $('#plot-btn').attr('disabled', 'disabled');
    $('#tag-select').empty();
    var tagMask = $('#tagmask-text').val();
    if (tagMask.length > 0) {
        var populateTags = function (tags) {
            $.each(tags, function (name, webId) {
                $('#tag-select').append("<option value=\"" + name + "\">" + name + "</option>");
            });
            piTagWebIds = tags;
            $('#plot-btn').removeAttr('disabled');
        };
        var error = function () {
            alert("Error during tag search.");
        };
        piwebapi.GetTagList(tagMask, populateTags, error, errorCallBack);
    }
});

 

Testing the application, you can now enter a tag mask and see the list of tags populated:

 

Getting Plot Data

Next, we need to get the plot data from PI Web API and plot a time-series graph using Flot.

 

In the piwebapi_wrapper.js file, add a private function:

    // Normal HTTP request: get plot values based on webId
    var getValuesRequest = function (tagWebId, startTime, pixels, successCallBack, errorCallBack) {
        var url = basePIWebAPIUrl + "/streams/" + tagWebId + "/plot?intervals=" + pixels;
        if (startTime != null) {
            url += "&starttime=" + startTime;
        }
        return processJsonContent(url, "GET", null, successCallBack, errorCallBack);
    };

 

and a public function:

        // Get plot values based on webId
        GetValues: function (tagWebId, startTime, pixels, successCallBack, errorCallBack) {
            getValuesRequest(tagWebId, startTime, pixels, successCallBack, errorCallBack);
        }

 

These functions allow us to submit a GET request to the Streams Controller > GetPlot action.

 

In the app.js file, we need to transform the response content into a format consumable by our plotting library Flot.

 

Data in Flot is an array of data series:

    [ series1, series2, ... ]

 

In this case, we only have 1 series (1 PI tag). To plot time series data, we need to express our timestamps and values in an array as follow (represent 1 series):

    [ [time1, value1], [time2, value2], ... ]

 

After we received the response body, we will loop through each item and add the timestamp and value to an array if the quality of the value is “good”.

 

In app.js

var plotData = [[]];
var width = "800";

// Plot options
var options = {
    xaxis: {
        mode: "time",
        timeformat: "%Y/%m/%d %H:%M:%S",
        timezone: "browser"
    }
};

// Start plot
$('#plot-btn').click(function (data) {
    var tag = $('#tag-select option:selected').text();
    var timeInterval = $('#time-interval-text').val();
    var timeIntervalUnit = $('#time-interval-unit option:selected').val();

    if (tag.length > 0) {
        // Get initial values
        if (timeInterval.length <= 0 || isNaN(timeInterval)) {
            timeInterval = 1;
            $('#time-interval-text').val(timeInterval);
        }
        var startTime = "*-" + timeInterval + timeIntervalUnit;

        var successCallBack = function (values) {
            plotData = [[]];
            for (var i = 0; i < values.Items.length; i++) {
                if (values.Items[i].Good) {
                    plotData[0].push([new Date(values.Items[i].Timestamp).getTime(), values.Items[i].Value]);
                }
            }
            $.plot($('#plot'), plotData, options);
        };

        piwebapi.GetValues(piTagWebIds[tag], startTime, width, successCallBack, errorCallBack)

        $('#plot-btn').attr('disabled', 'disabled');
        $('#stop-btn').removeAttr('disabled');
    }
});

// Stop update
$('#stop-btn').click(function (data) {
    $('#plot-btn').removeAttr('disabled');
    $('#stop-btn').attr('disabled', 'disabled');
});

 

Testing the application, you should be able to specify a time interval, and plot the values for a specific PI tag until current time.

For now, we will disable the start button and enable the stop button after the start button is pressed (and vice versa) in preparation for our next section on channels demonstration. The idea is once the start button is pressed, the trend will be updating in real-time as the PI tag gets new values. For now, you can toggle the start and stop button to refresh the trend for a fast updating tag.

 

Channels CTP

Next, let’s make this plot update on its own when new values come in. With traditional REST HTTP requests, the client will have to initiate the request. This means that the client would have to periodically poll the server for new information. In PI Web API 2015 R3, channels are introduced. Channels use the WebSocket protocol to allow the PI Web API server send messages containing new stream value changes. Once a channel is opened, the client will receive continuous updates from the server without having to poll. This can be more efficient by reducing unnecessary requests, as well as reducing the overhead that comes with HTTP headers.

 

You can find out more information about channels in the PI Web API help file. Look at the GetChannel (and GetChannelAdHoc) action in the Stream and StreamSet controllers respectively. In addition, example usage is shown in the Channels Topic section.

 

Since I’m new to WebSocket, I decided to test out this functionality first using a pre-built tool available at https://www.websocket.org/echo.html. I have provided a URL for testing: wss://MyPIWebAPIServer/piwebapi/streams/{webId}/channel where {webId} is for an interface heartbeat tag.

 

I see new values coming in once the WebSocket is opened!

 

A few caveats that I have discovered:

  • Since WebSocket is still a relatively new technology, not all browsers (and browser versions) support WebSockets. (e.g. I tested this successfully on Chrome 46.0.2490.80)
  • If you have trouble connecting, open up the browser developer tools (usually F12) to take a look at the error message. If you see an authentication failure, try to open up a new tab in the same browser and navigate to any PI Web API endpoint (and enter credentials if necessary). This is because we cannot customize WebSocket headers from JavaScript currently, so we are relying on the authorization headers sent from the browser. (Here, I’m bypassing the limitation by using the basic authentication cached by the browser.)
  • Since response only comes in when a new value comes in for the tag; if your tag is slow updating, you will not see any response until a new value comes in. You can consider adding the includeInitialValues=true URL parameter to see the current value of the stream when the channel is opened.

 

Once we are familiar with the functionality of channels, let’s implement it in our JavaScript application.

 

In piwebapi_wrapper.js, add the following public variable:

var basePIWebAPIChannelUrl = "wss://YourPIWebAPIServer/piwebapi";

 

Add the following private variable in the module:

var webSocket = null;

 

Add the following private functions in the module:

    // Channels demonstration: open web socket based on webId
    var openStreamChannel = function (tagWebId, channelOpenCallBack, channelErrorCallBack, channelMessageCallBack, channelCloseCallBack) {
        var url = basePIWebAPIChannelUrl + "/streams/" + tagWebId + "/channel";
        webSocket = new WebSocket(encodeURI(url));
        webSocket.onopen = channelOpenCallBack;
        webSocket.onerror = channelErrorCallBack;
        webSocket.onmessage = channelMessageCallBack;
        webSocket.onclose = channelCloseCallBack;
    };

    // Channels demonstration: close web socket
    var closeStreamChannel = function () {
        if (webSocket != null) {
            webSocket.close();
        }
    };

 

Add the following public functions in the module:

       // Open channel for a stream
        OpenChannel: function (tagWebId, channelOpenCallBack, channelErrorCallBack, channelMessageCallBack, channelCloseCallBack) {
            openStreamChannel(tagWebId, channelOpenCallBack, channelErrorCallBack, channelMessageCallBack, channelCloseCallBack);
        },

        // Close existing opened channel
        CloseChannel: function () {
            closeStreamChannel();
        }

 

Add code to reset websocket in the public Reset function:

        Reset: function () {
            currentUserName = null;
            currentPassword = null;
            webSocket.close();
            webSocket = null;
        },

 

The above JavaScript simply opens a channel based on a specific URL, and allow for callbacks when the channel:

  • Just opened (onopen)
  • Encounters an error (onerror)
  • Receives a new message from the server (onmessage)
  • Just closed (onclose)

 

In app.js, add the following in the click handler for #plot-btn after calling piwebapi.GetValues:

        // Open channel
        var messageCallBack = function (event) {
            var values = JSON.parse(event.data);
            var plotStartTime = getPlotStartTime(timeIntervalUnit, timeInterval);

            // Remove old values before new start time
            var removeCount = 0;
            while (plotData[0][removeCount][0] < plotStartTime.getTime()) {
                removeCount++;
            }
            plotData[0].splice(0, removeCount);

            // Add new values
            for (var i = 0; i < values.Items[0].Items.length; i++) {
                if (values.Items[0].Items[i].Good) {
                    var timestamp = new Date(values.Items[0].Items[i].Timestamp);
                    if (timestamp.getTime() >= plotStartTime.getTime()) {
                        plotData[0].push([timestamp.getTime(), values.Items[0].Items[i].Value]);
                    }
                }
            }

            // Sort array
            plotData[0].sort(function (a, b) {
                if (a[0] === b[0]) {
                    return 0;
                }
                else {
                    return (a[0] < b[0]) ? -1 : 1;
                }
            });

            // Plot
            $.plot($('#plot'), plotData, options);
        };

        var errorCallBack = function () {
            alert("Error getting updates.");
        };

        var openCallBack = function () {
            $('#search-btn').attr('disabled', 'disabled');
        };

        var closeCallBack = function () {
            $('#search-btn').removeAttr('disabled');
        };

        piwebapi.OpenChannel(piTagWebIds[tag], openCallBack, errorCallBack, messageCallBack, closeCallBack);

 

Note that we need to parse the string as JSON (by JSON.parse) returned by WebSocket. In addition, the above code does the following when a new value is received:

  • Removed values outside of the time interval of plotting
  • Add new values that are within the plotting interval
  • Sort arrays because there can be out-of-order data, and channel does not guarantee that the values are in chronological order.

 

The getPlotStartTime function is designed to get the start time of the plot by subtracting a specific interval from the current time:

 

// Subtract time interval from now
var getPlotStartTime = function (intervalUnit, intervalVal) {
    var ret = new Date();
    switch (intervalUnit.toLowerCase()) {
        case 'h': ret.setTime(ret.getTime() - intervalVal * 3600000); break;
        case 'm': ret.setTime(ret.getTime() - intervalVal * 60000); break;
        case 's': ret.setTime(ret.getTime() - intervalVal * 1000); break;
        default: ret = undefined; break;
    }
    return ret;
};

 

Finally, we need to close the channel when the stop update button is clicked.

// Stop update
$('#stop-btn').click(function (data) {
    piwebapi.CloseChannel();
    $('#plot-btn').removeAttr('disabled');
    $('#stop-btn').attr('disabled', 'disabled');
});

 

Testing the application with a fast moving tag:

liveupdate.gif

 

The trend is now live updating whenever new values come in! Note that this trend will not update unless there is a new value that comes in, so your trend can look stale if the tag is not updating frequently. If you do not have a fast updating tag, consider plotting a test tag and manually inputting data to the tag.

 

 

Conclusion

I hope you have as much fun using the new PI Web API 2015 R3 as I am! The full project is available in this GitHub repository. Please note that both batch and channel are currently CTP releases, which means that if you would like to see them as official release, please give us your comments and feedback!

As I was trying to write a C# application that uses PI Web API and looking up various ways to make a REST call in .NET applications, I came across the following methods to consume REST APIs in .NET:

  • WebClient
  • HttpWebRequest
  • HttpClient
  • Other libraries (e.g. RestSharp)

 

It looks like the new API that Microsoft offers, HttpClient (namespace: System.Net.Http), provides some powerful functionality with better syntax support for newer threading features. For more information about the differences of various ways to make REST calls, please refer to the following resources:

 

Looking through the pros and cons of each, I have decided to give HttpClient a try. In the following blog post, we will be writing a wrapper around HttpClient for easy access to PI Web API. Note that this is not a comprehensive HttpClient wrapper for PI Web API, the idea is to share some development thoughts while testing the class out. If you need a quick and easy way to access PI Web API in your development project, you can use this class and focus on the data access logic.

 

The following example is created in Visual Studio 2013, .NET 4.5 and tested with PI Web API 2015 R2.

 

 

Getting Started

First, we will create a wrapper. Start a new Visual Studio project and name the class PIWebAPIClient.

 

namespace piwebapi_cs_helper
{
    public class PIWebAPIClient
    {
    }
}

 

Since we will be handling JSON objects, let’s add the Newtonsoft.Json package from the package manager console:

Install-Package Newtonsoft.Json

 

And add the using directive:

using Newtonsoft.Json.Linq;

 

Let’s also add a reference to System.Net.Http and add the using directive since we are testing the HttpClient class!

using System.Net.Http

 

 

Creating and Disposing the HttpClient

In the following, we will write constructors and methods to initialize a HttpClient intended to be used to make multiple HTTP requests.

 

        private HttpClient client;

        /* Initiating HttpClient using the default credentials.
         * This can be used with Kerberos authentication for PI Web API. */
        public PIWebAPIClient()
        {
            client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
        }

        /* Initializing HttpClient by providing a username and password. The basic authentication header is added to the HttpClient.
         * This can be used with Basic authentication for PI Web API. */
        public PIWebAPIClient(string userName, string password)
        {
            client = new HttpClient();
            string authInfo = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(String.Format("{0}:{1}", userName, password)));
            client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authInfo);
        }

        /* Disposing the HttpClient. */
        public void Dispose()
        {
            client.Dispose();
        }

 

As a simple example, we did not add any additional headers for the HttpClient. Feel free to add headers appropriate for your application. In addition, although HttpClient does implement the IDisposable interface, many MSDN examples did not explicitly call Dispose(). We will include it for completion sake.

 

 

Asynchronous GET and POST request

We are ready to make HTTP requests! Since PI Web API only returns JSON objects by default, we will return a NewtonSoft JObject from our async GET method. For POST, we will be accepting a string payload in JSON format. If the response message indicates that the request is not successful, a HttpRequestException will be thrown with the response message.

 

        /* Async GET request. This method makes a HTTP GET request to the uri provided
         * and throws an exception if the response does not indicate a success. */
        public async Task<JObject> GetAsync(string uri)
        {
            HttpResponseMessage response = await client.GetAsync(uri);
            string content = await response.Content.ReadAsStringAsync();
            if (!response.IsSuccessStatusCode)
            {
                var responseMessage = "Response status code does not indicate success: " + (int)response.StatusCode + " (" + response.StatusCode + " ). ";
                throw new HttpRequestException(responseMessage + Environment.NewLine + content);
            }
            return JObject.Parse(content);
        }

        /* Async POST request. This method makes a HTTP POST request to the uri provided
         * and throws an exception if the response does not indicate a success. */
        public async Task PostAsync(string uri, string data)
        {
            HttpResponseMessage response = await client.PostAsync(uri, new StringContent(data, Encoding.UTF8, "application/json"));
            string content = await response.Content.ReadAsStringAsync();
            if (!response.IsSuccessStatusCode)
            {
                var responseMessage = "Response status code does not indicate success: " + (int)response.StatusCode + " (" + response.StatusCode + " ). ";
                throw new HttpRequestException(responseMessage + Environment.NewLine + content);
            }
        }

 

At times, you might need to submit other HTTP requests to PI Web API (e.g. PUT, PATCH). While you can use HttpClient.PutAsync for a PUT request, it doesn’t have a method to support PATCH request out-of-the-box. If you are in a situation to make a PATCH call, there are many online examples to do so. For more information, refer to this previous discussion.

 

 

(Optional) Additional methods to make Synchronous Calls (e.g. Console Application)

If you are writing a simple console application and would like to synchronously call the GetAsync/PostAsync method, you can add the following methods:

 

        /* Calling the GetAsync method and waiting for the results. */
        public JObject GetRequest(string url)
        {
            Task<JObject> t = this.GetAsync(url);
            t.Wait();
            return t.Result;
        }

        /* Calling the PostAsync method and waiting for the results. */
        public void PostRequest(string url, string data)
        {
            Task t = this.PostAsync(url, data);
            t.Wait();
        }

 

 

Testing synchronous calls in a Console application

Let’s build the solution and test!

 

First, let’s add references to our PIWebAPIClient helper, as well as to Newtonsoft.Json.

using piwebapi_cs_helper;
using Newtonsoft.Json.Linq;

 

and write a simple console application. The following console application accepts a URL (i.e. the PI Web API REST endpoint) and prints out the response message. Note that we are using the PIWebAPIClient constructor that uses the default credentials. This method works when your PI Web API instance is set up using Kerberos.

namespace piwebapi_cs_console_test
{
    class Program
    {
        /* Console application that makes GET request to a specified URL and display the response
         * as a string to the console. */
        static void Main(string[] args)
        {
            PIWebAPIClient piWebAPIClient = new PIWebAPIClient();
            do
            {
                try
                {
                    Console.Write("Enter URL: ");
                    string url = Console.ReadLine();
                    JObject jobj = piWebAPIClient.GetRequest(url);
                    Console.WriteLine(jobj.ToString());
                }
                catch (AggregateException ex)
                {
                    foreach (var e in ex.InnerExceptions)
                    {
                        Console.WriteLine(e.Message);
                    }
                }
                finally
                {
                    Console.WriteLine("Press any key to continue (esc to exit)...");
                }

            } while (Console.ReadKey().Key != ConsoleKey.Escape);
            piWebAPIClient.Dispose();
            Console.ReadKey();
        }
    }
}

 

 

As you can see, we can make simple synchronous requests!

 

 

Testing asynchronous calls in a WPF application

Next, we will write a WPF application to test making asynchronous GET and POST requests. Again, add references to the following:

using piwebapi_cs_helper;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Net.Http;

 

If you are interested, the WPF xaml configuration is as follow:

<Window x:Class="piwebapi_cs_wpf_test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="325" Width="500">
    <Grid Margin="0,0,0,11">
        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="300" />
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="User name" />
        <Label Grid.Row="1" Grid.Column="0" Content="Password" />
        <Label Grid.Row="2" Grid.Column="0" Content="PI Point path" />
        <Label Grid.Row="3" Grid.Column="0" Content="Value to write" />
        <TextBox Grid.Row="0" Grid.Column="1" Margin="3"  Name="userNameTextBox" />
        <PasswordBox Grid.Row="1" Grid.Column="1" Margin="3" Name="pwBox" />
        <TextBox Grid.Row="2" Grid.Column="1" Margin="3"  Name="tagTextBox" />
        <TextBox Grid.Row="3" Grid.Column="1" Margin="3"  Name="valueTextBox" />
        <Button Grid.Row="4" Grid.Column="1" Margin="5" Content="Write" Name="writeBtn" Click="writeBtn_Click"/>
        <TextBlock Grid.Row="5" Grid.Column="1" Margin="3" Name="statusTextBlock" TextWrapping="WrapWithOverflow" />
    </Grid>
</Window>

 

The window accepts username and password combination and use basic authentication to connect to PI Web API. Any error (or success) messages are displayed in the bottom of window. It will first try to use the tag path specified to get to the endpoint that accepts a value-writing POST request. In this simple example, we will only be supplying the value in the payload. This means that the value will be written in current time.

namespace piwebapi_cs_wpf_test
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        /* This method takes the username and password specified to use basic authentication to connect to 
         * PI Web API. It then attempts to resolve the tag path provided and write to the tag. */
        private async void writeBtn_Click(object sender, RoutedEventArgs e)
        {
            string baseUrl = "https://dng-code.osisoft.int/piwebapi";
            string userName = userNameTextBox.Text;
            string password = pwBox.Password;
            string tagPath = tagTextBox.Text;
            PIWebAPIClient piWebAPIClient = new PIWebAPIClient(userName, password);

            try
            {
                //Resolve tag path
                string requestUrl = baseUrl + "/points/?path=" + tagPath;
                Task<JObject> tget = piWebAPIClient.GetAsync(requestUrl);
                statusTextBlock.Text = "Processing...";

                //Attempt to write value to the tag
                Object payload = new
                {
                    value = valueTextBox.Text
                };
                string data = JsonConvert.SerializeObject(payload);
                JObject jobj = await tget;
                await piWebAPIClient.PostAsync(jobj["Links"]["Value"].ToString(), data);

                //Display final results if successful
                statusTextBlock.Text = "Write success!";
            }
            catch (HttpRequestException ex)
            {
                statusTextBlock.Text = ex.Message;
            }
            catch (Exception ex)
            {
                statusTextBlock.Text = ex.Message;
            }
            finally
            {
                //We are closing the HttpClient after every write in this simple example. This is not necessary.
                piWebAPIClient.Dispose();
            }
        }
    }
}

 

Let's try to write to a tag!

 

Conclusion

The full VS 2013 solution (including the test project) is in the GitHub repository piwebapi-cs-helper. HttpClient looks pretty easy to use so far, and it works well with PI Web API. I am curious to see what others in the community have been using to access a PI Web API (or other REST API) in C#. Comments and feedback are welcome!

The new PowerShell Tools for the PI System have been released! These PowerShell cmdlets are installed with the PI System Management Tools 2015 (3.5.1.7). You might recall that we had a CTP release (version 1.1.0.0) a few years ago, available only to vCampus members. The current version is a rewrite of the previous release, and offer many functional and performance enhancements over the CTP version. It is also available to all licensed customers and PI DevClub members.

 

These PowerShell Tools are designed to provide PI System Administrators the ability to create reusable scripts for common or bulk system management operations. While it is not primarily a data access tool, I will be testing out some data read and write operations to get familiar with the new tools. I am by no means a PowerShell expert: the purpose of the blog post is to bring awareness to the tools available and offer a quick start guide to anyone who is interested!

 

Getting Started

Before we start, please note that PowerShell Tools for the PI System requires Windows PowerShell 4.0 (see release notes). You can check your PowerShell version by running

$PSVersionTable.PSVersion

 

After an installation or upgrade to PI SMT 2015, the PowerShell tools are available as a PowerShell module named OSIsoft.PowerShell. Beginning in Windows PowerShell 3.0, modules are automatically imported when you use a command in the module. Since it is no longer provided as a PSSnapin as in the CTP version, there is no need to calll Add-PSSnapin. This version of the PowerShell Tools is installed in %pihome%\OSIsoft.PowerShell. The path should already been added to the PSModulePath environment variable:

$env:PSModulePath

 

You should see your %pihome% directory listed.

 

To confirm that the OSIsoft.PowerShell module is imported in the current session, run

Get-Module

and look for OSIsoft.PowerShell under the name column. However, the OSIsoft.PowerShell module will only show up if a command in the module has previously been used in the session. If you don't see OSIsoft.PowerShell module in the list, run a command (e.g. Get-Command -Module OSIsoft.PowerShell) and then run Get-Module to check again.

 

To get a list of all the cmdlets available in the OSIsoft.PowerShell module:

Get-Command -Module OSIsoft.PowerShell

 

From the resulting list, you might have noticed that there are no cmdlets available to access PI AF (which were previously included in the vCampus CTP version). The AF cmdlets are actively being developed now and are planned for inclusion in the next release. For more information, please refer to KB01248.

Note: AF Server support to PowerShell Tools for the PI System has been added in the SMT 2015 R2 release!

 

To get the description and example for specific cmdlets, run the Get-Help cmdlet. E.g.

Get-Help Connect-PIDataArchive

 

You can specify specific information to obtain in Get-Help by supplying different parameters (e.g. -examples, -full, etc.).

 

Connecting to the PI Data Archive

First, let's create a connection to the PI Data Archive. Let's check out the Connect-PIDataArchive cmdlet:

Get-Help Connect-PIDataArchive

 

DESCRIPTION

    The Connect-PIDataArchive cmdlet allows a connection to be made to the specified PI Data Archive.  A PI Data Archive can be specified by machine name, or entry in the local Known Servers Table.

 

It looks like we can make a connection to the PI Data Archive by name! (You might recall the CTP version requires us to get a PI Server object first.) To see specific examples:

Get-Help Connect-PIDataArchive -Examples

 

Now, let's attempt a connection (make sure you have the appropriate Windows mapping/trust set up):

$myPI = Connect-PIDataArchive -PIDataArchiveMachineName DNG-PI2012

 

$myPI.Connected should return true. We have successfully connected to our PI Data Archive!

 

Getting some values

Next, let’s try to get some values from a PI tag. Let’s see what are the available cmdlets which contain the word “value” as the noun:

Get-Command -Noun *value* -Module OSIsoft.PowerShell

 

CommandType     Name                                          ModuleName

-----------               ----                                               ----------

Cmdlet               Add-PIValue                                  OSIsoft.PowerShell

Cmdlet               Get-PIValue                                  OSIsoft.PowerShell

Cmdlet               Remove-PIValue                            OSIsoft.PowerShell

Ah! Let's use Get-PIValue. (Don't forget to use Get-Help to look for examples if needed!)

 

One way to get the current value of sinusoid:

Get-PIValue -PointName sinusoid -Connection $myPI -Time ([DateTime]::Now)

 

AnnotationHandle :     0

Value                  :     89.76907

TimeStamp          :     7/20/2015 5:45:40 PM

WriteMode           :      NoReplace

UpdateType         :      None

Annotation           :

IsServerError        :      False

IsQuestionable     :      False

IsSubstituted        :      False

IsAnnotated          :      False

IsGood                :      True

StreamId              :      1

Notice that the TimeStamp is displayed as UTC time.

 

As another example, let's get the list of compressed values of sinusoid between 8 am - 5 pm on 7/19/2015:

Get-PIValue -PointName sinusoid -Connection $myPI -StartTime "19-Jul-2015 8:00:00" -EndTime "19-Jul-2015 17:00:00" 

 

To format the results into an easily viewable format, you can append

  • Format-Table; or
  • Select-Object -Property TimeStamp, Value (or Select TimeStamp, Value); etc.

at the end of the command. E.g.

Get-PIValue -PointName sinusoid -Connection $myPI -StartTime "19-Jul-2015 8:00:00" -EndTime "19-Jul-2015 17:00:00" | Select TimeStamp, Value

 

TimeStamp                                                                                                         Value

---------                                                                                                                 -----

7/19/2015 12:37:23 PM                                                                                         0.9707313

7/19/2015 1:42:23 PM                                                                                             3.3811

7/19/2015 2:56:23 PM                                                                                           23.64582

7/19/2015 5:30:23 PM                                                                                            85.4732

7/19/2015 6:39:23 PM                                                                                           99.19289

7/19/2015 7:44:23 PM                                                                                           96.29641

7/19/2015 8:57:23 PM                                                                                            75.9825

Notice that only the TimeStamp and Value columns are display. Again, TimeStamps are in UTC time (I am at UTC-4 right now).

 

Writing some values

Recall that there is a Add-PIValue cmdlet that looks to be suitable for writing values to PI tags.

 

To add a value of 10 to a tag TestTag at current time:

Add-PIValue -PointName TestTag -Value 10 -Connection $myPI

 

By default, the write mode of "append" is used" unless specified otherwise with the -WriteMode parameter. In addition, you can specify the timestamp, or write different values to multiple tags at the same time. The Get-Help examples contain detailed information.

 

Conclusion

Even though this version of the PowerShell Tools is a re-write of the vCampus CTP version, a lot of the cmdlets and parameters are similar. If you are interested, I encourage checking out some excellent blog posts in the past about using the PowerShell Tools to carry out different tasks:

 

If you have already upgraded to PI SMT 2015, test out some of the cmdlets! As always, please leave your comments and feedback

 

Notes (as of Aug 2015)

  • For timestamps, PI relative times (e.g. *) cannot be used.
  • Data writes with Add-PIValue cannot be buffered right now. There is an enhancement request in place to allow buffered writes in a future release.

PI AF 2.x allows the creation of custom data references, which extend the capability of PI AF by defining the information and mechanism necessary for an AF Attribute to read, calculate, and/or write a value. There are many reasons to implement a custom data reference (e.g. displaying data in AF from an external data source, custom calculations – though we encourage checking out asset analytics first!). In the background, data references are simply .NET classes that derive from the OSIsoft.AF.Asset.AFDataReference base class that are registered and stored in the AF Server. If you haven’t worked with custom data references before, please check out the white paper “Implementing AF 2.x Data References” available for download from the Tech Support Download Center.

 

The purpose of this blog post is to discuss a less-documented feature, which is to implement AF Data Pipes for custom data references. An AF Data Pipe is simply a collection of AF Attribute objects that are signed up for data change events on the server. It is often used by clients to get all data change events for AF Attributes. This question was first brought up in a thread: Custom Data Reference - How To Implement Data Pipe? After realizing that there is limited information about this topic, I have decided to test out implementing an AF Data Pipe.

 

 

Background

There are two types of data source for the AF Data Pipe (AFDataPipe): calculation or system of record. AFDataPipe considers a data reference a calculation data source if it has one or more inputs. For a calculation data reference to support data pipe, it must have at least one input supporting DataPipe.

 

In general, for a data reference plugin to support AFDataPipe:

  • If the data reference is not system of record and depends on AFSDK to get the inputs data to the data reference, AFSDK can handle getting the inputs and call AFDataReference.GetValue to compute the output for the data pipe event.
  • If the data reference is system of record, then it can implement a derived class of AFEventSource and expose a static CreateDataPipe method in the plugin to return this derived AFEventSource.

 

 

My Custom Data Reference

In this blog, I will be implementing a simple data reference based on a system of record to illustrate how to support AFDataPipe. This data reference, called SQLDR, simply gets the timestamps and values from the specified SQL table for display in PI AF.

 

Please feel free to skip this section if you are familiar with implementing a custom data reference. I’m using AF SDK 2.6.2 in this example.

 

Creating the Visual Studio Project

  1. Create a new Class Library using the .NET Framework 4.5.
  2. Add a reference to the OSIsoft.AFSDK.dll located at %pihome%\AF\PublicAssemblies\4.0 folder.
  3. Rename the class name (I renamed mine to SQLDR.cs).

 

Preparing our new class library to implement AFDataReference

Add the using directive:

using OSIsoft.AF.Asset;

 

Add inheritance for SQLDR to AFDataReference:

public class SQLDR : AFDataReference

 

Note that each custom AFDataReference class implementation requires a unique System.Runtime.InteropServices.GuidAttribute attribute to be specified to uniquely identify the data reference in AF. A System.SerializableAttribute and System.ComponentModel.DescriptionAttribute are also required on the class before an AFDataReference can be used in AF.

 

using System.ComponentModel;
using System.Runtime.InteropServices;

 

[Serializable]
[Guid("A1AC3A39-9E55-4700-BBC0-68299E67C4A1")]
[Description("SQLDR; Get values from a SQL table")]
public class SQLDR : AFDataReference

 

 

Implementing AFDataReference

ConfigString

There are 3 pieces of information we need for our custom data reference: the name of the SQL server, the database, as well as the name of the table containing the data. We will implement a simple configuration where these information are stored in a string separated by semicolons.

 

// Private fields storing configuration of data reference
private string _tableName = String.Empty;
private string _dbName = String.Empty;
private string _sqlName = String.Empty;

// Public property for name of the SQL table
public string TableName
{
    get
    {
        return _tableName;
    }
    set
    {
        if (_tableName != value)
        {
            _tableName = value;
            SaveConfigChanges();
        }
    }
}

// Public property for name of the SQL database
public string DBName
{
    get
    {
        return _dbName;
    }
    set
    {
        if (_dbName != value)
        {
            _dbName = value;
            SaveConfigChanges();
        }
    }
}

// Public property for name of the SQL instance
public string SQLName
{
    get
    {
        return _sqlName;
    }
    set
    {
        if (_sqlName != value)
        {
            _sqlName = value;
            SaveConfigChanges();
        }
    }
}

// Get or set the config string for the SQL data reference
public override string ConfigString
{
    get
    {
        return String.Format("{0};{1};{2}", SQLName, DBName, TableName);
    }
    set
    {
        if (value != null)
        {
            string[] configSplit = value.Split(';');
            SQLName = configSplit[0];
            DBName = configSplit[1];
            TableName = configSplit[2];
            SaveConfigChanges();
        }
    }
}

 

 

GetValue/GetValues

Next, we need to tell AF what values to retrieve and display when the client requests for values for the data reference. Since we will be getting data from a SQL server, let’s write a separate SQLHelper class.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data;
using System.Data.SqlClient;

namespace AFSDK_CustomDR_SQL
{
    /*****************************************************************************************
     * SQL Helper class to get data from the specified SQL Server, SQL Database and SQL Table. 
     * Given a start and end time, the static GetSQLData method will return a SQLDataReader
     * object that contains all values between the time interval in ascending order.
     * Given the end time only, only the most current value will be returned. 
     *****************************************************************************************/
    class SQLHelper
    {
        public static SqlDataReader GetSQLData(string sqlServer, string sqlDb, string sqlTable, DateTime startTime, DateTime endTime)
        {
            // Construct connection string to SQL Server based on input parameters for SQL server name and database name.
            string connectString = String.Format("server={0}; database={1}; Integrated Security=SSPI; Connection Timeout=10", sqlServer, sqlDb);
            SqlConnection sqlConnection = new SqlConnection(connectString);

            // Construct SQL query
            string query;
            using (SqlCommand cmd = new SqlCommand())
            {
                cmd.Connection = sqlConnection;

                // SQL query for the most recent values before the end time
                if (startTime == DateTime.MinValue)
                {
                    query = String.Format("SELECT TOP 1 pi_time, pi_value FROM {0}.{1} WHERE pi_time <= @time ORDER BY pi_time DESC", sqlDb, sqlTable);
                    SqlParameter sqlTime = cmd.Parameters.Add(new SqlParameter("time", System.Data.SqlDbType.DateTime2));
                    sqlTime.Value = endTime;
                    cmd.CommandText = query;
                }

                // SQL query for all values over a specified time range
                else
                {
                    query = String.Format("SELECT pi_time, pi_value FROM {0}.{1} WHERE pi_time >= @startTime AND pi_time <= @endTime ORDER BY pi_time ASC", sqlDb, sqlTable);
                    SqlParameter sqlStartTime = cmd.Parameters.Add(new SqlParameter("startTime", System.Data.SqlDbType.DateTime2));
                    SqlParameter sqlEndTime = cmd.Parameters.Add(new SqlParameter("endTime", System.Data.SqlDbType.DateTime2));
                    sqlStartTime.Value = startTime;
                    sqlEndTime.Value = endTime;
                    cmd.CommandText = query;
                }

                /* Open SQL connection and return the SqlDataReader object. Use CommandBehavior.CloseConnection to ensure that the 
                 * SQL connection is closed when the SqlDataReader object is closed. */
                sqlConnection.Open();
                SqlDataReader sqlReader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
                return sqlReader;
            }
        }
    }
}

 

 

Now, we are ready to override the GetValue and GetValues methods in our main SQLDR class.

 

Add the using directive:

using System.Data;
using System.Data.SqlClient;
using OSIsoft.AF.Time;

 

Override GetValue and GetValues:

// Return latest value if timeContext is null, otherwise return latest value before a specific time
public override AFValue GetValue(object context, object timeContext, AFAttributeList inputAttributes, AFValues inputValues)
{
    AFValue currentVal = new AFValue();
    DateTime time;
    if (timeContext != null)
    {
        time = ((AFTime)timeContext).LocalTime;
    }
    else
    {
        time = DateTime.Now;
    }
    using (SqlDataReader reader = SQLHelper.GetSQLData(SQLName, DBName, TableName, DateTime.MinValue, time))
    {
        if (reader.Read())
        {
            currentVal.Timestamp = AFTime.Parse(reader["pi_time"].ToString());
            currentVal.Value = reader["pi_value"];
        }
    }

    return currentVal;
}

// Return all values (converted to AFValues) over a specific time interval
public override AFValues GetValues(object context, AFTimeRange timeRange, int numberOfValues, AFAttributeList inputAttributes, AFValues[] inputValues)
{
    AFValues values = new AFValues();
    DateTime startTime = timeRange.StartTime.LocalTime;
    DateTime endTime = timeRange.EndTime.LocalTime;
    using (SqlDataReader reader = SQLHelper.GetSQLData(SQLName, DBName, TableName, startTime, endTime))
    {
        while (reader.Read())
        {
            AFValue newVal = new AFValue();
            newVal.Timestamp = AFTime.Parse(reader["pi_time"].ToString());
            newVal.Value = reader["pi_value"];
            values.Add(newVal);
        }
    }
    return values;
}

 

 

Note that we could also be implementing Rich Data Access (RDA) methods such as RecordedValue, InterpolatedValue, etc. These were omitted in this short example. However, I encourage you to check out his PI Developers Club discussion about the topic: When would one need to implement AFDataMethods on a custom DR?

 

SupportedMethods, SupportedDataMethods and SupportedContexts

Finally, we will specify the minimal list of supported methods for our simple custom data reference.

 

Add the using directive:

using OSIsoft.AF.Data;

 

Override SupportedMethods, SupportedDataMethods, and SupportedContexts:

public override AFDataReferenceMethod SupportedMethods
{
    get
    {
        return AFDataReferenceMethod.GetValue | AFDataReferenceMethod.GetValues;
    }
}

public override AFDataMethods SupportedDataMethods
{
    get
    {
        return AFDataMethods.None;
}

public override AFDataReferenceContext SupportedContexts
{
    get
    {
        return AFDataReferenceContext.Time;
    }
}

 

 

Testing

To test our SQLDR data reference so far, let’s register it using the RegPlugIn.exe utility. Navigate to %pihome%\AF directory and run the following command:

Regplugin /PISystem:your-af-server “C:\path\to\dll”

 

Verify that the plug-in is registered by running:

Regplugin /PISystem:your-af-server /List

 

regplugin-list.png

 

Finally, open up PI System Explorer and create a new Attribute using our custom data reference SQLDR!

 

CDRTest.png                    CDRTest2.png

 

 

Implementing the Data Pipe

Alright! We have a working data reference. We are now ready to implement AF Data Pipe.

 

If you try to sign up for updates, you will not receive any updates because the data reference does not support AFDataPipe currently.

 

There are a few things we will need to do:

  1. Implement AFEventSource with AddSignup, RemoveSignup, GetEvents() and Dispose(bool). AFEventSource is a public abstract class providing a basis for data reference developers to implement the Data Pipe feature. The base class will handle all the interaction with AF SDK pipe modules, and implementer does not have to worry about interfacing with different data sinks.
  2. Add a static method, CreateDataPipe, in your implementation of AFDataReference to return an AFEventSource object that you have implemented.
  3. Explicitly state support for data pipe by changing the SupportedDataMethods to include the AFDataMethod.DataPipe.

 

1. Inheriting AFEventSource

The AFEventSource base class has methods to publish data pipe events and report errors, as well as maintains a dictionary of the AFAttributes being monitored by the eventsource. We will create a new class (EventSource.cs) which inherits from the AFEventSource base class.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.Data;
using OSIsoft.AF.Time;

namespace AFSDK_CustomDR_SQL
{
    /*****************************************************************************************
     * The EventSource class implements AFEventSource to specify how to get data pipe events 
     * from the system of record. 
     *****************************************************************************************/
    class EventSource : AFEventSource
     {

 

 

Storing Timestamps

First, we would like to store the start time when the data pipe was initiated, as well as the last timestamp where data was retrieved for each AF Attribute. Since there can be a list of AFAttributes signed up for the data pipe, we will keep a dictionary of <AFAttribute, AFTime> to record the last timestamps for each AF Attribute.

 

// Last timestamps for each AF Attribute
Dictionary<AFAttribute, AFTime> _lastTimes = new Dictionary<AFAttribute, AFTime>();

// Start time when the pipe is initiated
AFTime _startTime;

// Initialize the start time for the event source
public EventSource()
{
    _startTime = new AFTime("*");
}

 

 

The GetEvents method

Next, we will implement the GetEvents method, which is designed to get data pipe events from the system of record. The base class maintains a dictionary of the AFAttributes being monitored by the eventsource. The idea here is to publish AFDataPipeEvents between the last timestamps till current time, for each AF Attribute that is signed up for the data pipe.

 

// Get new events for the pipe from the last timestamps till current time of evaluation
protected override bool GetEvents()
{
    // Set evaluation time to current time
    AFTime evalTime = AFTime.Now;

    // Get the list of AF Attributes signed up on the data pipe
    IEnumerable<AFAttribute> signupList = base.Signups;

    // Get values for each AF Attribute, one at a time
    foreach (AFAttribute att in signupList)
    {
        if (!ReferenceEquals(att, null))
        {
            // Add AF Attribute if it hasn't been added to the _lastTimes dictionary yet
            if (!_lastTimes.ContainsKey(att))
            {
                _lastTimes.Add(att, this._startTime);
            }

            // Set time range to get all values between last timestamps to current evaluation time
            AFTimeRange timeRange = new AFTimeRange(_lastTimes[att], evalTime);

            /* Note: Use RecordedValues if supported. GetValues call return interpolated values at the start and end time,
             * which can be problematic in a data pipe implementation. GetValues is used here for this simple example because
             * the implementation of GetValues in my custom DR does not return interpolated values at the start and end time. */
            AFValues vals = att.GetValues(timeRange, 0, att.DefaultUOM);

            // Store old last time for the AF Attribute
            AFTime lastTime = _lastTimes[att];

            // Publish each value to the data pipe
            foreach (AFValue val in vals)
            {
                // Record latest timestamp
                if (val.Timestamp > lastTime)
                {
                    lastTime = val.Timestamp;
                }
                AFDataPipeEvent ev = new AFDataPipeEvent(AFDataPipeAction.Add, val);
                base.PublishEvent(att, ev);
            }

            // Add a tick to the latest time stamp to prevent the next GetValues call from returning value at the same time
            _lastTimes[att] = lastTime + TimeSpan.FromTicks(1);
        }
    }
    return false;
}

 

 

The Dispose method

Finally, we will override the dispose method to clean up objects.

 

// Dispose resources
protected override void Dispose(bool disposing)
{
    _lastTimes = null;
}

 

 

2. Add the CreateDataPipe method

At the main SQLDR class, we will add a static method CreateDataPipe that returns an AFEventSource object.

 

// Return an AFEventSource object for this custom data reference
public static object CreateDataPipe()
{
    EventSource pipe = new EventSource();
    return pipe;
}

 

 

3. Include Data Pipe in Supported Data Methods

Last but not least, we need to tell clients that data pipe is supported for our custom data reference. We will therefore change the SupportedDataMethods to include DataPipe.

 

public override AFDataMethods SupportedDataMethods
{
     get
    {
        return AFDataMethods.DataPipe;
    }
}

 

 

Testing Data Pipe Functionality

To test, let’s rebuild our solution and re-register the data reference to the AF server. Remember RegPlugIn? We will be using it to first un-register the data reference:

RegPlugIn /PISystem:your-af-server name-of-dll.dll /unregister

 

(You can find the name of the dll by inspecting the output of RegPlugIn /PISystem:your-AF-server /List)

 

Then, we will reregister using the same command as before:

Regplugin /PISystem:your-af-server “C:\path\to\dll”

 

Make sure the SQLDR attribute still looks good in PSE! (Reopen PSE to make sure that the latest data reference version is downloaded.)

 

This time, we will be testing the event pipe functionality in a custom AF SDK application. Before testing, I have created 3 attributes: one using PI Point data reference mapping to the PI Tag CDT158 (cdt158), and two attributes using our custom data reference SQLDR (SQLDRTest and SQLDRTest2). The code for the test is taken from Marco’s blog post: Using data pipes with future data in PI AF SDK 2.7. I will not go into the detailed implementation here; nonetheless, here are the results:

datapipe_results.png

 

Notice our SQLData Reference gets AFDataPipe updates, just as a PI Point data reference! (You might be wondering why the values of the custom data reference is identical to CDT158. The truth is, I am using the PI Interface for RDBMS to write the value of CDT158 to a SQL table, then using the custom data reference to read from the same SQL table. So the fact that we see the same value updates for CDT158 shows that my data pipe is working!)

 

 

Limitations in this Example

Note that in the above example, we are looping through each AFAttribute and making a single RecordedValues call. If you have implemented RDA methods and your data source supports bulk calls, you can do a bulk call on AFAttributeList in the GetEvents method. The downside is that you will be using the same time range for your bulk call, which may not apply for your data reference. If you want to keep track of the list of AFAttributes signed up for the data pipe, you can implement AddSignUp and RemoveSignUp in AFEventSource as well.

 

 

Things to Note

When implementing data pipes, be careful not to retrieve the same event twice. You will notice that we are adding a tick to lastTime in order to get around this issue. In addition, we are using SQLDbType.DateTime2 (instead of DateTime) to add higher precision when doing value retrieval.

 

 

Debugging

If you need to debug your custom data reference at any time during development, check out the following resources:

(Note that the second reference is written specifically for debugging custom delivery channel, but the general principals can be applied to debugging custom data references.)

 

 

Conclusion

I hope this blog post is useful to you. The full VS 2013 solution (including the test project) is in the GitHub repository afsdk-customdr-sql. Please feel free to give me any feedbacks and comments!

PI ProcessBook 2014 introduces a new AF Display Builder add-in that provides visualization to the AF hierarchy (see KB01122 for more information). The AF Display Builder allows users to easily explore PI AF elements and attributes as well as create symbols in PI ProcessBook. Recently, I was assisting a customer in building a PI ProcessBook docking window that displays the AF hierarchy. When users double-click on any element, the .pdi file associated with the element will be opened. I figure I will share this application here in case someone is looking to build a docking window for other purposes.

 

Part I of the blog post will show how to add some simple AF UI controls to a docking window in PI ProcessBook, while Part II will show how to modify the controls to open a .pdi file when an element is double-clicked.

 

 

Part I: Building a PI ProcessBook Docking Window with some AF UI Controls

 

Using the PI ProcessBook Add-In Templates

 

We provide VB.NET and C# templates for creating PI ProcessBook 3.x add-ins. These templates are available for download in the Tech Support Download Center. Since we are developing a docking window in C#, after downloading the templates, unzip and place the AddInPBDockWin_CS file in %USERPROFILE%\Documents\Visual Studio 2013\Templates\ProjectTemplates\Visual C# folder.

 

In Visual Studio 2013, create a new project and select the AddInPBDockWin_CS template:

addtemplate.png

 

Investigating the Docking Window Add-In Template

 

Let’s briefly investigate the docking window add-in project that we have just created from the template. For detailed information, please refer to the document “PI ProcesBook AddIn Templates for VS2008.doc” that comes with the template download.

 

Connect.cs

The Connect class contains IDTExtensibility2 methods that all PI ProcessBook add-ins must implement. The two main methods that can be changed are:

  1. OnConnection
    • Specifies the load behavior when PI ProcessBook initializes the add-in.
  2. OnDisconnection
    • Specifies any cleanup needed when PI ProcessBook terminates the add-in.

 

AddinReg.reg

This file contains the registry keys that PI ProcessBook uses to locate and initialize add-ins. While we are here, let’s edit the “FriendlyName” and “Description” field. Also, make sure that the registry path is correct (see the "extra" section below for more information).

addinreg.png

 

Adding References to PI AF SDK and AF UI

 

Let’s add some references so we can access objects from the AF Database. In addition, we will make use of some ready-made controls available from the AF UI so we don’t have to reinvent the wheel.

  • AF SDK (from %pihome%\AF\PublicAssemblies\4.0\OSIsoft.AFSDK.dll)
  • AF UI (from %pihome%\AF\PublicAssemblies\4.0\OSIsoft.AF.UI.dll)

Since we are adding the .NET 4 version of the PI AF SDK and AF UI to the project, please ensure that the project is targeting .NET framework 4.5.

 

 

Adding the AF UI to a .NET User Control

 

We will next add a .NET user control and put some AF UI components in it. (Thanks to Steve Pilon’s post which gave many useful guidelines!)

 

1.     Right-click the PB_DockWin_AFTreeView project and add a user control. We’ll name it AFTreeControl.cs:

     usercontrol.png

 

2.     In the design mode, resize the user control. We will then add 3 AF UI controls:

    • PISystemPicker: to allow user select and connect to an AF server
    • AFDatabasePicker: to allow user select and connect to a database in the AF server selected
    • AFTreeView: to display the AF element hierarchy

 

     For more information about AF UI components, refer to the AF User Interface Library Reference located at %pihome%\help\AFUIRef.chm.

 

     Modify the PISystem picker and AFDatabasePicker to remove unneeded controls. E.g.:

     afPickerProp.png

 

     The resulting user control will look something like this:

     usercontrol2.png

 

3.     Open the code behind the user control and make the user control visible to COM:

 

using System.Runtime.InteropServices;

 

namespace PB_DockWin_AFTreeView
{
    [ComVisible(true)]
    public partial class AFTreeControl : UserControl
    {

 

 

Adding functionality to the AF UI Controls

 

Let’s add code to our user control (AFTreeContol.cs) to allow us to populate the AFTreeView with the element hierarchy when an AF Server and AF Database is selected.

 

To begin, add using directives to the referenced AF SDK and AF UI:

 

using OSIsoft.AF;
using OSIsoft.AF.UI;

 

We would like the AFDatabasePicker (named afDbPicker) to populate the list of databases whenever an AF server is selected from the PISystemPicker (named afPicker) control.

 

// Initialize the AF UI Controls
public AFTreeControl()
{
    InitializeComponent();
    afDbPicker.SystemPicker = afPicker;
}

 

Next, we need populate the AFTreeView when users select/change the AF Database using AFDatabasePicker. We will add code to react to the SelectionChange event:

  • Click on the AFDatabasePickerControl and select Events under the Properties Window.
  • Double-click on SelectionChange:

     eventchange.png

 

Visual Studio will automatically populates code needed to handle this event. Back in the User Controls code, we see that afDbPicker_SectionChange has been added. Add the following code to get the elements to display in the AFTreeView (named afTreeView):

 

// Change the elements in the TreeView to the selected AF system and AF Database
private void afDbPicker_SelectionChange(object sender, SelectionChangeEventArgs e)
{
    afTreeView.AFRoot = null;
    AFDatabase myDatabase = afDbPicker.AFDatabase;
    if (myDatabase != null && myDatabase.Elements != null)
        afTreeView.AFRoot = myDatabase.Elements;
}

 

Now, our control should display the AF element hierarchy after an AF server and database has been selected.

 

 

Displaying the User Control in PI ProcessBook

 

The last step would be to ask the docking window add-in to load our user control.

 

Let’s clean up the default label added as an example in the docking window template:

  • Remove the label control and the associated variable m_strAddInName
  • Change the name of the docking window to “AFTreeView Docking Window”
  • Add our user control (AFTreeControl) to the view in the docking window

 

The edited OnConnection method looks like the following:

 

public void OnConnection(object Application, Extensibility.ext_ConnectMode ConnectMode, object AddInInst, ref System.Array custom)
{
    try
    {
        m_theApp = (PBObjLib.Application)Application;

        #region docking window

        m_dockWindow = m_theApp.DockWindows.Add(PBObjLib.pbDockWindowType.pbTypeSingleViewWindow, "AFTreeView Docking Window", PBObjLib.pbDockPosition.pbDockLeft, false);

        if (m_dockWindow != null)
        {
            m_dockWindow.Visible = true;

            //ensure the tree view is fully visible
            m_dockWindow.Width = 250;

            //create a view in the docking window to contain the control
            PBObjLib.PBControlView ctrlView = (PBObjLib.PBControlView)m_dockWindow.Views.Add(PBObjLib.pbViewType.pbControlViewType);

            AFTreeControl treeControl = (AFTreeControl)ctrlView.CreateControl("PB_DockWin_AFTreeView.AFTreeControl");
        }

        #endregion

    }
    catch (Exception ex)
    {
        MessageBox.Show("Exception in OnConnection=" + ex.Message);
    }
}

 

 

Finally, we are ready to test! Note that the Debug configuration specify the start action as opening PI ProcessBook. Make sure to change the hard-coded path to reflect the actual location of the PI ProcessBook executable on your machine.

debugprop.png

 

Clicking Debug will register the add-in in the registry (remember AddReg.reg?) and launch PI ProcessBook. The docking window should now be loaded automatically!

dockingwin.png

 

 

Part II: Additional Functionality

 

We have successfully added a AFTreeView in a PI ProcessBook docking window! We are ready to add some custom functionality!

 

As I mentioned in the beginning of this blog post, the goal of this application is to open up a .pdi file when user double-click on any element. To do that, we will parse the path of the element by accessing the node that has been double-clicked. If a .pdi file with the same name already exists, the file will be opened; if not, a new file will be opened.

 

To open up a display at a double-click event, the user control needs to know about the PI ProcessBook application.

 

// The processBook application
public PBObjLib.Application app; 

 

We can then set the ProcessBook Application in the OnConnection method during initialization:

 

AFTreeControl treeControl = (AFTreeControl)ctrlView.CreateControl("PB_DockWin_AFTreeView.AFTreeControl");
treeControl.app = m_theApp;

 

(By the way, this is by no means perfect. Please feel free to comment/improve upon this!)

 

 

Let’s add code to react to a NodeMouseDoubleClick event: (make sure to first create an event handler for the event as described above for the SectionChange event for the AFDatabasePicker)

 

using System.IO;

 

// Open a new display when the node is double-clicked
private void afTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
{
     // Parse element path to get the name of the element
    string[] path = e.Node.FullPath.Split('\\');
    string filePath = String.Format("C:\\Dev Support\\VBA in PB\\{0}.pdi", path[path.Length - 1]);

     // If the pdi exists, open the file
    if (File.Exists(filePath))
    {
        app.Displays.Open(filePath, false);
    }
     // If pdi doesn't exist, add a dislpay with the name of the element
    else
    {
        app.Displays.Add(path[path.Length - 1]);
    }
}

 

 

Rebuild the project and test the new functionality in PI ProcessBook!

 

 

Extra: A Few Tips about Deployment

 

Note that PI ProcessBook reads the list of add-ins from the registry at

  • HKLM\SOFTWARE\Wow6432Node\PISystem\PI – ProcessBook\Addins, or
  • HKLM\SOFTWARE\Software\PISystem\PI – ProcessBook\Addins

 

depending on the bitness. It is important to modify the AddinReg.reg file so that the registry keys are added in the correct location.

 

To modify the load/unload behavior, you can open up Add-In Manager in PI ProcessBook (Tools > Add-In Manager) and modify the default loading properties for your add-in:

addinmanager.png

 

Finally, if you have finished development and would like to deploy your add-in on a client machine, please refer to KB00592.

 

 

Conclusion

 

The full project is in the GitHub repository pb-dockwin-aftreeview. Feedback and comments are always welcomed!

When Asset-Based Analytics was first released with PI Asset Framework 2014 last year, Marcos wrote an excellent blog post about how to programmatically interact with expression analyses. Over the course of the year, we have seen some interests in accessing, editing or creating analyses programmatically.

 

Before we start, I would like to stress that the creation and maintenance of AF analyses is best accomplished by using the Analyses Management plug-in in PI System Explorer. You can find detailed information in our user guides for Asset-Based Analytics. In addition, we strongly encourage the use of analysis templates, which take advantage of the AF hierarchy and is a much more efficient way to create analyses. Every element derived from an element template automatically acquires analyses from its analysis templates.

 

In some scenario, you might want to make bulk edits to attribute names, time rules, or exporting analysis configuration into another application. In this blog post, I will add a few code snippets on:

  • Accessing and creating rollup analyses
  • Changing natural and periodic time rules
  • Viewing, editing and creating event frame generation analyses

 

The following is tested with PI AF SDK 2.6.2.

 

 

Rollup Analyses

 

Unlike expression analyses which store the equation/expression in the config string of the analysis rule:

 

   Figure 1: Expression analysis

 

AnalysisRule.ConfigString gives Average := (‘Temperature’ + ‘Temperature2’)/2

 

Rollup analyses store the configurations in variable mappings:

     Figure 2: Rollup analysis

 

AnalysisRule.VariableMapping gives SearchCriteria||Elements[@Name=*]|Attributes[@Name=Level];Sum||Level_Rollup;

 

In fact, the ConfigString is configuration specific to the AnalysisRule, while the VariableMapping is PI AF SDK infrastructure that is common to all AnalysisRules. The reason Rollup doesn't have any config string is that the analysis it performs is fixed, it's just a matter of "wiring up" the inputs/outputs to attributes.

 

 

To programmatically create a roll-up analysis (one that is identical to Figure 2), we can create a new analysis with the analysis rule plug in type of "RollUp" and set the variable mapping according to the format observed above. E.g.

 

//Add rollup analysis with name "Rollup_Calc"
AFAnalysis rollupAnalysis = tanks.Analyses.Add("Rollup_Calc");

// Define analysis rule plug-in
rollupAnalysis.AnalysisRulePlugIn = myAF.AnalysisRulePlugIns["Rollup"];

// Define the variable mappings
AFAttribute rollupLevel = tanks.Attributes["Level_Rollup"];
string rollupSearch = "Elements[@Name=*]|Attributes[@Name=Level]";
rollupAnalysis.AnalysisRule.MapVariable("SearchCriteria",rollupSearch);
rollupAnalysis.AnalysisRule.MapVariable("Sum", rollupLevel);

/* An alternative way to define the variable mappings using the AFVariableMap object. It provides a richer object model for examining and updating the variable mapping configuration.
AFVariableMap rollupVarMap = rollupAnalysis.AnalysisRule.VariableMap;
AFVariableMappingData rollupSearchMap = new AFVariableMappingData(rollupSearch);
AFVariableMappingData rollupLevelMap = new AFVariableMappingData(rollupLevel);
rollupVarMap.SetMapping("SearchCriteria", rollupSearchMap);
rollupVarMap.SetMapping("Sum", rollupLevelMap);
*/

// Define periodic time rule with frequency of 5 minutes
rollupAnalysis.TimeRulePlugIn = myAF.TimeRulePlugIns["Periodic"];
rollupAnalysis.TimeRule.ConfigString = "Frequency=300";

// Enable and check in newly created analysis
rollupAnalysis.SetStatus(AFStatus.Enabled);
rollupAnalysis.CheckIn();

 

 

Natural and Periodic Time Rules

 

In the above example, we have created a periodic time rule such that the analysis runs every 5 minutes. Let's examine how we can change it to a natural time rule.

 

The structures for periodic and natural time rule differ slightly:

PeriodicNatural
Time rule plug inPeriodicNaturalNatural
Time rule config stringFrequency=300;Offset=10(null)"attribute1";"attribute2"
CommentsFrequency and offset are represented in seconds (offset is optional)Analysis triggered on any inputTriggered on one or more input attribute

 

For the above rollup analysis (Figure 2), if we would like to change from periodic to natural time rule (and trigger on any input):

 

AFAnalysis rollupAnalysis = tanks.Analyses["Rollup_Calc"];
rollupAnalysis.TimeRulePlugIn = myAF.TimeRulePlugIns["Natural"];
rollupAnalysis.SetStatus(AFStatus.Enabled);
rollupAnalysis.CheckIn();

 

 

Event Frame Generation Analyses

 

Before attempting to programmatically construct an Event Frame Generation analysis, let’s first inspect the analysis rule structure by looking at an existing event frame analysis.

   Figure 3: Event Frame Generation analysis

 

To inspect the analysis, the easiest way would be to look at the exported XML file for the element where the analysis rule belongs to:

 

<AFAnalysis>
    <Name>EF_Calc</Name>
    <Status>Enabled</Status>
    <Target>
        <AFElementRef>.</AFElementRef>
    </Target>
    <AFAnalysisRule>
        <AFPlugIn>EventFrame</AFPlugIn>
        <ConfigString>EFTGUID=49b65f5d-17a9-4ddf-8fe2-68d0b55a92a8;EFTNAME=EFTemplate</ConfigString>
        <AFAnalysisRule>
            <AFPlugIn>PerformanceEquation</AFPlugIn>
            <ConfigString>StartTrigger:= 'Temperature' &gt; 200;</ConfigString>
        </AFAnalysisRule>
        <AFAnalysisRule>
            <AFPlugIn>TimeTrue</AFPlugIn>
            <ConfigString>TimeTrueValue=0</ConfigString>
        </AFAnalysisRule>
    </AFAnalysisRule>
    <AFTimeRule>
        <AFPlugIn>Natural</AFPlugIn>
    </AFTimeRule>
</AFAnalysis>

 

 

Alternatively, we can look at the Analysis.AnalysisRule in Visual Studio:

ef3.png

 

From both, we can see the following:

  • The analysis rule plug in type is "EventFrame"
  • The config string for the analysis rule is of the format:
    • EFTGUID=<GUID of event frame template>;EFTNAME=<name of event frame template>
  • The start trigger (end trigger) and time true are stored as child analysis rules within the event frame analysis rule
    • Start trigger:
      • Plug in type: PerformanceEquation
      • Config string format: StartTrigger:= <equation>;
    • Time true:
      • Plug in type: TimeTrue
      • Config string format: TimeTrueValue=<value>

 

With these information, we can create our Event Frame Generation Analysis programmatically. The configuration of this analysis is identical to the one shown in Figure 3.

 

// Obtaining the event frame template
AFElementTemplate efTemplate = myDB.ElementTemplates["EFTemplate"];

// Add event frame generation analysis with the name "EF_Calc"
AFAnalysis efAnalysis = tanks.Analyses.Add("EF_Calc");

// Define analysis rule plug in and build config string
efAnalysis.AnalysisRulePlugIn = myAF.AnalysisRulePlugIns["EventFrame"];
efAnalysis.AnalysisRule.ConfigString = String.Format("EFTGUID={0};EFTNAME={1}", efTemplate.UniqueID, efTemplate.Name);

// Define plug in type and config string for child analysis rules
AFAnalysisRule triggerRule = efAnalysis.AnalysisRule.AnalysisRules.Add(myAF.AnalysisRulePlugIns["PerformanceEquation"]);
triggerRule.ConfigString = "StartTrigger:= 'Temperature' > 200;";
AFAnalysisRule timeTrueRule = efAnalysis.AnalysisRule.AnalysisRules.Add(myAF.AnalysisRulePlugIns["TimeTrue"]);
timeTrueRule.ConfigString = "TimeTrueValue=0";
            
// Define natural time rule to trigger at any input
efAnalysis.TimeRulePlugIn = myAF.TimeRulePlugIns["Natural"];
efAnalysis.SetStatus(AFStatus.Enabled);
efAnalysis.CheckIn();

 

 

Conclusion

 

At the very least, I hope this blog post can give you a glimpse into the configuration of rollup and event frame generation analyses. Feedback and comments are always welcomed!

 

 

EDIT (6/11/2015): Courtesy of David Moler and Mike Zboray, I have edited the blog post to incorporate their comments. Please keep the comments coming!

The Programming Hackathon 2015 has come to a fruitful end (check out the winners here!). During the Hackathon, we have provided resources for several new and exciting technologies:

  • PI Server 2015 with Future Data
  • PI Web API
  • PI Integrator for Esri
  • Esri ArcGIS APIs
  • PI Integrator for BI and Azure Machine Learning

 

During the event, we received a lot of feedback about making the links to the online resources available for these technologies after the event. Even if you didn't have a chance to attend the Programming Hackathon, we encourage you to explore the following resources to get started on your next project!

 

PI Server 2015 - Future data

 

PI Web API

 

PI Integrator for Esri ArcGIS

 

Esri ArcGis APIs

 

PI Integrator for BI and Azure Machine Learning

AF 2.x Clients contain an executable named RegPlugIn.exe used for registering plug-in assemblies, typically located in the \PIPC\AF directory. There are currently 4 types of PI AF Plug-Ins: DataReference, DeliveryChannel, TimeRule, and AnalysisRule. In addition, there may be support assemblies that are required for the implementation of the AF Plug-Ins or to provide translated resources for different languages. Starting in AF 2.1, support (or dependent) assemblies can be loaded by RegPlugIn as well.

 

The goal of this blog is to walk you through a simple example on the caveats of using RegPlugIn to register support assemblies. The following examples are tested with PI AF 2014 R2.

 

Background on the RegPlugIn Utility

 

The simplest way to register an AF Plug-In is to run the RegPlugIn utility located in the \PIPC\AF folder:

RegPlugIn Path\To\MyPlugIn.dll

which uploads the plug-in to the AF server. When an AF client (e.g. PI System Explorer) needs the Plug-In, it will download the Plug-In on to the client computer in the %ProgramData%\OSIsoft\AF\PlugIns directory.

 

Path

.NET of the Plug-In

Plug-Ins

PlugIns (root)

.NET 3.5

version Shipped with AF client

PlugIns\4.0

.NET 4+

version Shipped with AF client

PlugIns\x.x.x.x

.NET 3.5

dll version of the custom plug-In

PlugIns\4.0\x.x.x.x

.NET 4+

dll version of the custom plug-In

 

The above applies for plug-ins targeting “Any CPU”. If the plug-in specifically targets “x86”, it will be placed under the x86 folder under its respective directory; similarly, plug-ins targeting “x64” will be placed under the x64 folder.

 

After registering the plug-in, you can list the registered assemblies by running:

RegPlugIn /List


There are other parameters (e.g. you can specify the PI AF server) available for the RegPlugIn utility. For examples and parameters, I recommend checking out the “Managing plug-ins” section in the PI System Explorer User Guide.

 

My First Attempt at Registering a Support Plug-In (Do not Follow!)

 

In this example, I will use a custom delivery channel that I developed to output alerts at specific Twitter handle (TwitterDeliveryChannel.dll). As part of the project, I am referencing a support assembly that serves as a .NET wrapper to the Twitter API (LinqToTwitter.dll).

 

After compiling the class library, I am ready to register the delivery channel plug-in. I copied both assemblies from my development server to one of my AF client machines, under C:\Users\dng\Documents. After navigating to the \PIPC\AF directory, I first register my main Delivery Channel plug-in (TwitterDeliveryChannel.dll):

RegPlugIn “C:\Users\dng\Documents\TwitterDeliveryChannel.dll”


I then register the support assembly (LinqToTwitter.dll):

RegPlugIn “C:\Users\dng\Documents\LinqToTwitter.dll” /own:TwitterDeliveryChannel.dll


The /own or /owner parameter is used to specify the name of the owner assembly during support assembly registration. Alternatively, you can register both assemblies at the same time:

RegPlugIn “C:\Users\dng\Documents\TwitterDeliveryChannel.dll” “C:\Users\dng\Documents\LinqToTwitter.dll” /own:TwitterDeliveryChannel.dll

 

The following message shows that my assembly has been successfully registered. Note that the support assembly is registered at the relative path (“Users\dng\Documents\LinqToTwitter.dll”):

img1.png

This can be further confirmed by running RegPlugIn /List:

img2.png

However, when I tried to use my new Twitter Delivery Channel on a client machine, I encountered this error:

img3.png

On the client machine, the TwitterDeliveryChannel.dll is downloaded to the %ProgramData%\OSIsoft\AF\PlugIns\1.0.0.0 directory (version of the dll). Interestingly, the LinqToTwitter is located at %ProgramData%\OSIsoft\AF\PlugIns\1.0.0.0\Users\dng\Documents.

 

When I run Process Explorer to look at which directories PI Notifications is trying to find the supporting assembly (LinqToTwitter.dll):

img4.png

It looks like PI Notifications Manager is only looking for the supporting assembly at the PlugIns (root) and PlugIns\version directories. Since my supporting assembly is located at PlugIns\1.0.0.0\Users\dng\Documents, it cannot be found or loaded.

 

When (and how) are the assemblies loaded?

 

When a PI AF SDK client needs the plug-in (e.g. creating a delivery channel end point), it will download the necessary dll (e.g. TwitterDeliveryChannel.dll) from the AF server if the client does not have the plug-in with the same (or higher) version. It then calls loadLibrary to load the dll from the directory path according to the dll specifications (e.g. .NET 3.5 or 4+, x86/x64, and version).

 

On the other hand, PI AF SDK does not load the supporting dll (e.g. LinqToTwitter.dll) directly. Microsoft runtime (or .NET framework) is responsible to load any referenced dll when it is required. Since my custom delivery channel does not explicitly call loadLibrary to load the required dll by specifying the relative path, the runtime will search in the GAC and through the default search path. Since Users\dng\Documents is not in the search path. The support dll is not loaded.

 

RegPlugIn uses the same path for both the input and output file location for supporting assemblies

 

When using RegPlugIn to register an AF plug-in, the file name (defaulted to current directory) or the path name is needed to specify where the dll is that you are trying to register. For the main plugin dll, the location of the plug-in in the client machine is determined strictly by the dll specifications.

 

However, for support dlls, the file path argument is used for both input and output specification. The file path is used by RegPlugIn to find where the dll is located, as well as to specify the relative path where the supporting assembly will be downloaded on the client machine (in this case, Users\dng\Documents). I was therefore implicitly specifying the relative output path when I was trying to register the supporting assembly!

 

How to Register Supporting Plug-Ins? (A Better Way)

 

The easiest way to register support assemblies is to put them in the same directory as the main plug-in dll. This way, they will be downloaded to the same directory onto the AF client machine. Rather than specifying the input path for the dll as the input arguments for RegPlugIn.exe, navigate to the directory containing the dlls and run:

“%pihome%\AF\RegPlugIn.exe” TwitterDeliveryChannel.dll LinqToTwitter.dll /own:TwitterDeliveryChannel.dll


Then, use RegPlugIn /list to verify that the plug-in and its supporting dlls are put in the same directory:

img5.png

You can also verify on the client machine that the plugin and its supporting dlls are downloaded into the expected directory.

 

Note that the search path could be modified by the %path% environment variable or through the application config file. The directory where the calling program or dll is loaded is also path of the search path. You can also hardcode the relative directory path to load the supporting dll. However, I find the easiest way to register support assembly is to put them in the same directory as the main plug-in dll.

 

Conclusion

 

When invoking RegPlugIn, set the default directory to where the input dlls are located and explicitly specify the path of RegPlugIn.exe. I hope you find this blog post helpful!

Filter Blog

By date: By tag: