Introduction

 

Recently, I have been teaching some advanced development courses for partners and customers. One of the topics is about developing PI Coresight custom symbols. It is really interesting the feedback I receive from the class. They really feel that with PI Coresight extensibility they can develop anything they want to. And this is really true. As long as you know about AngularJS, HTML5, JavaScript, there is a lot you can do in order to add value to your business through those custom symbols.

 

This blog post is a great example. There are a lot of customers who has asked some guidelines about integrating the PI System with. R. Why? R is not only good making complex calculation but also generate great graphics. With PI Coresight extensibility, custom symbols could be developed in order to show nice graphics generated with PI data. In this blog post, for a given amount of attributes, a graphic showing the correlation among those attributes will be displayed. As prerequisite, a custom ASP.NET Web API web service needs to be created in order to achieve this goal.

 

Here is how the final custom symbol looks like for 3 attributes:

 

 

You can find the source code package from this blog post here.

 

This blog post is the Chapter 11 of the White Paper - Integrating the PI System with R. As a result, please refer to this document for more information about R.NET and R.

 

 

Developing the custom ASP.NET Web API project with R.NET

 

Let's start developing the custom ASP.NET Web API. By adding to this project PI AF SDK, Svg, R.NET and R.NET Graphics, this RESTful web service can return SVG content from HTTP requests that the PI Coresight custom symbol can consume.

 

On the R.NET GitHub, there is a sample code for using R.NET in ASP.NET, which was used for me to get started. I also recommend you to take a look at this Visual Studio project. Although the SvgGraphicsContext.cs and SvgGraphicsDevice.cs files were initially copied from this repository, some small changes were made in order to work better.

 

Note that the RDotNet.Graphics NuGet package library is not released yet. The library is still an alpha version that works pretty well. Concerning the Svg library, the most recent version does not work well with RDotNet.Graphics library. Please use version 2.0.0.0 instead.

 

Let's start looking at the QueryData class, which is the input of our main action:

 


namespace RMultWebService.Models
{
    public class QueryData
    {
        public int Width { get; set; }
        public int Height { get; set; }
        public string StartTime { get; set; }
        public string EndTime { get; set; }
        public string Interval { get; set; }
        public string[] Paths { get; set; }




        public QueryData()
        {




        }
    }
}

 

 

This means that the custom symbol will have to provide to our RESTful web service:

 

1)Width and Height of the symbol node

2)Time range that you want to analyze and generate the multi-correlation graphics

3)Paths from all the attributes

 

Let's see our controller and its unique action:

 

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Http;
using RDotNet;
using RDotNet.Graphics;
using Svg;
using RMultWebService.Models;
using System.Web.Http.Cors;
using System.Web;
using System.Drawing.Imaging;
using System.Diagnostics;
using System.Configuration;






namespace RMultWebService.Controllers
{
    public class CodeController : ApiController
    {
        private static REngine _engine = null;
        private static SvgGraphicsDevice GraphicsDevice = null;
        private static int lastWidth = -1;
        private static readonly object _object = new object();


        public CodeController()
        {
            if (_engine != null)
            {
                return;
            }
            
                _engine = REngine.GetInstance(null, true, null, null);
                _engine.Initialize();          
                string rFilePath = ConfigurationManager.AppSettings["rFunctionPath"];
                _engine.Evaluate("source(\"" + rFilePath + "\")");
                GraphicsDevice = new SvgGraphicsDevice(new SvgContextMapper(400, 400, SvgUnitType.Pixel, null));
                _engine.Install(GraphicsDevice);
            
        }


        [HttpPost]
        public IHttpActionResult Execute(QueryData queryData)
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
          
            try
            {
                if ((queryData.Paths == null) || (queryData.Paths.Count() < 2))
                {
                    throw new Exception("There should be at least 2 attributes within the symbol.");
                }
                if (queryData.Paths.All(m => m.Substring(0, 2) == "af") == false)
                {
                    throw new Exception("PI Points are not accepted");
                }
                IEnumerable<string> plots = null;
                RApplication app = new RApplication();
                app.GetPIData(queryData.Paths, queryData.StartTime, queryData.EndTime, queryData.Interval);


                lock (_object)
                {
                    System.Threading.Thread.Sleep(1000);
                    if (lastWidth != queryData.Width)
                    {
                        GraphicsDevice = new SvgGraphicsDevice(new SvgContextMapper(queryData.Width, queryData.Height, SvgUnitType.Pixel, null));
                        _engine.Install(GraphicsDevice);
                        lastWidth = queryData.Width;
                    }                 


                    app.GenerateGraphic(_engine);
                    plots = GraphicsDevice.GetImages().Select(RenderSvg);
                }
                return Ok(plots);


            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message + "|\n" + ex.Source + "|\n" + ex.StackTrace);
            }
            finally
            {


                watch.Stop();
            }


        }


        private static string RenderSvg(SvgDocument image)
        {
            using (var stream = new MemoryStream())
            {
                image.Write(stream);
                stream.Position = 0;
                using (var reader = new StreamReader(stream))
                {
                    var contents = reader.ReadToEnd();
                    return contents;
                }
            }
        }


    }
}

 

The main action checks if QueryData.Paths is valid. This collection should have at least two items, which should all be attributes (each path string must start with "af"). Those paths are provided by the PI Coresight web services.

Then, an RApplication object is instantiated and the GetPIData() retrieves PI Data.

 

The next step is to send the data to R and generate the graphics. Nevertheless, in order to avoid exceptions, this code snippet should be written inside a lock(_object). This will make sure that there won't be two requests running inside this piece of code simultaneously. Note that in order to generate the Svg content, the GraphicsDevice needs to be "installed" on the REngine. If the width is changed when compared to the last request, a new GraphicsDevice needs to be created and replaced.

 

The RApplication class was taken from Chapter 9 of the White Paper and was simplified as only one function will be used. The content of this class is shown below:

 

using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using RDotNet;
using RDotNet.Graphics;
using System.IO;
using System.Diagnostics;
using OSIsoft.AF;
using OSIsoft.AF.Asset;
using OSIsoft.AF.Time;
using OSIsoft.AF.PI;
using System.Configuration;






namespace RMultWebService
{
    public class RApplication
    {


        private PIValuesList piValuesList = null;
        private readonly DateTime unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);


        public RApplication()
        {
            PISystems piSystems = new PISystems();
            string piSystemName = ConfigurationManager.AppSettings["piSystemName"];
            PISystem piSystem = piSystems[piSystemName];
        }


        public void GetPIData(string[] paths, string startTime, string endTime, string interval)
        {
            string[] correctPaths = paths.Select(m => m.Substring(3)).ToArray();
            AFKeyedResults<string, AFAttribute> results = AFAttribute.FindAttributesByPath(correctPaths, null);
            AFAttributeList attributeList = new AFAttributeList();
            foreach (AFAttribute attribute in results)
            {
                attributeList.Add(attribute);
            }


            AFTime start = new AFTime(startTime);
            AFTime end = new AFTime(endTime);
            AFTimeRange timeRange = new AFTimeRange(start, end);
            AFTimeSpan timeSpan = AFTimeSpan.Parse(interval);
            IEnumerable<AFValues> valueResults = attributeList.Data.InterpolatedValues(timeRange, timeSpan, string.Empty, false, new PIPagingConfiguration(PIPageType.TagCount, 100));
            piValuesList = new PIValuesList(valueResults);
        }


        public void GenerateGraphic(REngine engine)
        {
            for (int i = 1; i <= piValuesList.Count; i++)
            {


                double[] values = ConvertValuesToDoubleArray(piValuesList[i - 1]);
                double[] ts = ConvertTSToDoubleArray(piValuesList[i - 1]);
                NumericVector tagval = engine.CreateNumericVector(values);
                engine.SetSymbol("tag" + i.ToString() + "val", tagval);
                NumericVector tag_tsd = engine.CreateNumericVector(ts);
                engine.SetSymbol("tag" + i.ToString() + "tsd", tag_tsd);
                engine.Evaluate("tag" + i.ToString() + "ts<-as.POSIXct(tag" + i.ToString() + "tsd, origin='1970-01-01')");
                engine.Evaluate("tag" + i.ToString() + "<- data.frame(tag" + i.ToString() + "ts,tag" + i.ToString() + "val)");
            }
            int[] arrayHelper = Enumerable.Range(1, piValuesList.Count).ToArray();
            string[] tagnames = piValuesList.Select(m => "\"" + m.Name + "\"").ToArray();
            string impactString = string.Join(",", arrayHelper.Select(m => "tag" + m + "$tag" + m + "val"));
            string tnString = string.Join(",", tagnames);


            engine.Evaluate("impact<-data.frame(" + impactString + ")");
            engine.Evaluate("tn<-c(" + tnString + ")");
            engine.Evaluate("PI_Multi_Correlation(impact,tn)");
        }


        private double GetUTCFormat(DateTime DateFormat)
        {
            double UtcFormat = 1;
            TimeSpan ts = DateFormat - unixEpoch;
            UtcFormat = (ts.TotalMilliseconds) / 1000;
            return UtcFormat;


        }




        private double[] ConvertTSToDoubleArray(PIValues piValues)
        {
            return piValues.Select(val => GetUTCFormat(val.Timestamp)).ToArray();
        }




        private double[] ConvertValuesToDoubleArray(PIValues piValues)
        {
            return piValues.Select(val => Convert.ToDouble(val.Value)).ToArray();
        }
    }
}

 

Other classes such as PIValue.cs, PIValues.cs and PIValuesList.cs were also copied from Chapter 9.

 

Our custom web service to generate SVG content through R is ready!

 

According to  weather you are using 32-bit or 64-bit R binaries, you might want to set the "Enable 32-Bit Application" setting to True on IIS of the web server hosting your custom web service. Please refer to the white paper for more information.

 

 

 

 

Let's move to the custom PI Coresight symbol.

 

Developing the custom PI Coresight symbol

 

A great PI Coresight symbol starts with a great icon. Using public svg files for R, the r_multcorr.svg file was generated. The icon on PI Coresight is shown on the screenshot below and it is also available on GitHub:

 

 

On this blog post, the comments will be about what is specific for this custom symbol. General PI Coresight custom symbol development information can be found in many different resources, such as:

 

PI Coresight is developed on top of the AngularJS framework. The inject property of the definition object makes some common AngularJS services available on the init function, including the $http service for making RESTful web service calls. In this example. four services are injected:

    • $http --> this service is used for making a HTTP POST request against our custom web service with R.NET and PI AF SDK.
    • $sce --> this service is used to show the content of the SVG on DOM.
    • $timeout --> this service is used to wait some seconds before running a function.
    • $document --> a wrapper for windows.document.

 

You can click on any option above to read about the service directly on the AngularJS web site.

 

The code of the sym-rmult.js file is below:

 

(function (CS) {
    var definition = {
        typeName: 'rmult',
        datasourceBehavior: CS.DatasourceBehaviors.Multiple,
        iconUrl: 'Images/r-multcorr.svg',
        inject: ['$http', '$sce', '$timeout', '$document'],
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 400,
                Width: 400,
                Paths: null
            };
        },
        init: init
    };




    function init(scope, elem, $http, $sce, $timeout, $document) {


        var container = elem.find('#container')[0];
        var id = "rmult_" + Math.random().toString(36).substr(2, 16);
        container.id = id;
        scope.id = id;


        var timer;
        scope.resize = function (width, height) {
            $timeout.cancel(timer);
            timer = $timeout(function () {
                scope.config.Width = width;
                scope.config.Height = height;
                scope.showGraphic = false;
                scope.updateGraphic();
            }, 1000);
        }


        scope.updateGraphic = function () {


            if (scope.config.ElementsList == undefined) {
                return;
            }


            var postData = new Object();
            postData.StartTime = "1-Oct-2012";
            postData.EndTime = "1-Nov-2012";
            postData.Interval = "1h";
            postData.Width = scope.config.Width;
            postData.Height = scope.config.Width;
            postData.Paths = [];
            for (var i = 0; i < scope.config.ElementsList.length; i++) {
                postData.Paths.push(scope.config.ElementsList[i].Path);
            }
            if (postData.Paths.length < 2) {
                alert('The symnol must have at least 2 attributes!');
                return;
            }
            $http.post('http://marc-web-sql.marc.net:82/api/code', postData).then(function (response) {
                scope.plots = response.data;
                scope.plot = scope.plots[0];
                var currentElement = $document[0].getElementById(id)
                var currentElementWrappedID = angular.element(currentElement);
                currentElementWrappedID.height(scope.config.Width);
                scope.config.Height = scope.config.Width;
                scope.showGraphic = true;
            })
        }


        scope.dataUpdate = function (data) {
            if ((data == null) || (data.Rows.length == 0)) {
                return;
            }
            if (data.Rows[0].Path) {
                scope.config.ElementsList = data.Rows;
                scope.updateGraphic();
            }
        }


        scope.unsafe = function (s) {
            return $sce.trustAsHtml(s);
        }
        return { dataUpdate: scope.dataUpdate, resize: scope.resize };
    }
    CS.symbolCatalog.register(definition);
})(window.Coresight);

 

The content of the sym_rmult_template.html is below:

 

<div id="container" style="width:100%;height:100%;">
    <div ng-show="showGraphic==true" ng-bind-html="unsafe(plot)" style="width:100%;height:100%;background: aliceblue;"></div>
    <div ng-show="showGraphic==false" style="width:100%;height:100%;padding-top:40%;">
        <img src="../../Coresight/Images/loading-large-gray.gif" />
    </div>
</div>

 

 

Some comments about the code snippets above:

 

  • The initial lines of the init function has the goal of changing the id of the symbol in order to make it unique.
  • Yes, $http can also be used for making HTTP RESTful web service calls against PI Web API.
  • While the symbol is loading the SVG content from the web service, a gif image from PI Coresight is displayed by using the ng-show directive and checking the content of the $scope.showGraphic variable.
  • R.NET Graphics generate better graphics if the width is the same value of the height.
  • The data object received on the dataUpdate function is used only to get the paths of the attributes and not the values themselves. Those are retrieved by the custom web service using the paths array string and the time range.
  • Only AF Attributes work on this example. If a PI Point is added to the symbol, an exception will be throw and the response will have a status code of 500. But it is not difficult to update the code and make PI Points also compatible.
  • The paths from the attributes are stored on the scope.config.ElementsList.

 

Conclusions

 

I hope you have enjoyed this blog post and I would like to thank you for reading it. I have not tested how stable this is for production so if you decide to test it make please let us what you have found!