Skip navigation
All Places > PI Developers Club > Blog > 2016 > August

A while ago I was asked by our Japan team, "Hey Alex, do you have a Japanese version of our training Microsoft Azure VMs we can use for customer classes in Japan?"


... well, no, I didn't. So I decided to make some. I installed some Japanese Windows and Office language packs and PI System MUIs on our normal training template, moved it over to Japan and it worked great. Then I thought, "Why just Japanese? Couldn't I just do this for every language?". So I did. The end product means that students taking our training classes will be able to log in to their system in any language supported on the PI System, not just English.


There are a few traps, so thought I'd post about it here and hopefully help someone else that deals with many systems with many languages and locales. In my scenario, I was trying to configure a group of machines in an isolated domain so that they are able to be logged in with different accounts, and show the user a different language UI with each. For example, if a user logs in to the PI Server machine in the training environment with the Student01 account, they get an English PI and Windows UI. If they log in with fr-Student01 they get French, and with ja-Student01 they get Japanese. The steps I took to do this are approximately as follows:


  1. Start with an already configured PI System Client or Server Machine
  2. Install Windows and Microsoft Office language packs for each supported language
  3. Run the appropriate PI System MUI (Multilingual User Interface) products that correspond to the installed PI System software
  4. Configure new AD OUs for each language, and put new language specific accounts in each. Set up GPOs for each OU locking the user interface to the appropriate language.
  5. Establish startup scripts for all users, or on specific machines. This is the interesting bit, and something that my audience here may be interested in. There were a few difficulties I found when doing this. Specifically:
    1. The PI System MUI products don't seem to look at the user's language context to determine the language to display. They look at the machine language context. The user can change their language manually though, using the PI Language Settings Tool. I want to automate this.
    2. Older PI System UIs (Like the old Tag Search) do not use Unicode text. For these to be displayed properly in languages that use non-Latin characters, you need to change the whole system locale and reboot.


The commenting in my PowerShell script below should be of help for anyone else doing something like this. The below script runs for each user when they log in to a training account on each machine:


#Script written by Alex Duhig
#This is a startup script written to ease management of localized training VMs. Our training VMs can be logged in to with a range of accounts,
#each corresponding with a different language and region. The script detects the language of the user account according to a naming prefix,
#(i.e. de-Student01 = German) then configures time zones, the PI Language Setting tool and the Windows System Locale. Actual languages displayed
#on each account are not configured in this script, this is intended to be handled by Group Policy. 
#This script requires at least Windows 2012R2 or Windows 8.

#The below code gets administrator mode if it doesn't already have it and restarts the script.
If (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {   
  $arguments = "& '" + $myinvocation.mycommand.definition + "'"
  Start-Process powershell -Verb runAs -ArgumentList $arguments

#Find the current username, and the two letter language prefix if one exists.
$CurrentUser = [Environment]::UserName
$UserPrefix = $CurrentUser.substring(0,2)

#Extract user language from username (or assume english if no defined prefix)
IF (($UserPrefix -eq "de") -or ($UserPrefix -eq "es") -or ($UserPrefix -eq "fr") -or ($UserPrefix -eq "ja") -or `
   ($UserPrefix -eq "ko") -or ($UserPrefix -eq "pt") -or ($UserPrefix -eq "ru") -or ($UserPrefix -eq "zh")) {
    $UserLanguage = $UserPrefix
    $UserLanguage = "en"
Write-Host "User name is " $CurrentUser
Write-Host "User Language is " $UserLanguage

#The language prefixes need to be matched to a user locale. Our training classes are most commonly taught in these locales, so we'll 
#use them:
SWITCH ($UserLanguage) {
    "de"{$UserLocale = "de-DE"}
    "en"{$UserLocale = "en-US"}
    "es"{$UserLocale = "es-MX"}
    "fr"{$UserLocale = "fr-CA"}
    "ja"{$UserLocale = "ja-JP"}
    "ko"{$UserLocale = "ko-KR"}
    "pt"{$UserLocale = "pt-BR"}
    "ru"{$UserLocale = "ru-RU"}
    "zh"{$UserLocale = "zh-CN"}
Write-Host "User Locale is " $UserLocale

#Change the user locale so it matches their prefix
Set-Culture $UserLocale
Set-WinUserLanguageList -LanguageList (New-WinUserLanguageList $UserLocale) -Force

#Our training classes occur all over the world. A lot of languages are shared by multiple countries, and we'll leave them in UTC.
#However, it makes sense to change a few languages to a specific time zone. The following code changes the Time Zone Based on the 
#user language.
$PrevTimeZone = invoke-command {tzutil /g}
Write-Host "Current TimeZone: " $PrevTimeZone
SWITCH ($UserLanguage) {
    "de"{Invoke-Command {tzutil /s "W. Europe Standard Time"}}
    "en"{Invoke-Command {tzutil /s "UTC"}}
    "es"{Invoke-Command {tzutil /s "UTC"}}
    "fr"{Invoke-Command {tzutil /s "UTC"}}
    "ja"{Invoke-Command {tzutil /s "Tokyo Standard Time"}}
    "ko"{Invoke-Command {tzutil /s "Korea Standard Time"}}
    "pt"{Invoke-Command {tzutil /s "E. South America Standard Time"}}
    "ru"{Invoke-Command {tzutil /s "Russian Standard Time"}}
    "zh"{Invoke-Command {tzutil /s "China Standard Time"}}
$NewTimeZone = invoke-command {tzutil /g}
Write-Host "New Timezone: " $NewTimeZone

#The PI System MUIs sometimes don't detect the language of the user. Causes of this are usually linked to the OS being in a different language 
#than the current user. The below code works around this, and configures the registry manually for the user that has logged in.
#The below switch statement maps language prefixes to the numbers the PI System MUI uses in the registry.
SWITCH ($UserLanguage) {
    "de"{$UserSelectedDisplayLanguage = 7}
    "en"{$UserSelectedDisplayLanguage = 9}
    "es"{$UserSelectedDisplayLanguage = 10}
    "fr"{$UserSelectedDisplayLanguage = 12}
    "ja"{$UserSelectedDisplayLanguage = 17}
    "ko"{$UserSelectedDisplayLanguage = 18}
    "pt"{$UserSelectedDisplayLanguage = 22}
    "ru"{$UserSelectedDisplayLanguage = 25}
    "zh"{$UserSelectedDisplayLanguage = 4}
#When the PI Language Settings Tool is used to change language settings for a user, it uses a couple of registry keys. The below code changes
#these keys without input from a user. The "Set-ItemProperty" cmdlets are there in case the settings already exist.
New-Item -Path HKCU:\Software\PISystem\Common -Force | Out-Null
New-ItemProperty -Path HKCU:\Software\PISystem\Common -Name "IsPerUserLanguage" -Value 1 -PropertyType DWORD -Force | Out-Null
New-ItemProperty -Path HKCU:\Software\PISystem\Common -Name "UserSelectedDisplayLanguage" -Value $UserSelectedDisplayLanguage -PropertyType DWORD -Force | Out-Null
Set-ItemProperty -Path HKCU:\Software\PISystem\Common -Name "IsPerUserLanguage" -Value 1 -Force | Out-Null
Set-ItemProperty -Path HKCU:\Software\PISystem\Common -Name "UserSelectedDisplayLanguage" -Value $UserSelectedDisplayLanguage -Force | Out-Null

#Some older PI System products (Like the PI Tag Search GUI in PI ProcessBook) do not support Unicode languages, and rely on the locale of the 
#windows system to display characters properly. This is only a problem with non-Latin-character languages such as Japanese, Korean, Russian 
#and Chinese. The below code changes the system locale, but only bothers rebooting the machine if this login or the last login had a 
#non-Latin-character language. If everything is Latin, no reboot (or locale change at all really) should be needed.
$PreviousLocale = Get-WinSystemLocale
Write-Host "Previous Locale was " $PreviousLocale
Write-Host "Current User's Locale is " $UserLocale
IF ($PreviousLocale -ne $UserLocale) {
  Set-WinSystemLocale $UserLocale
        IF (($UserLocale -eq "ja-JP") -or ($UserLocale -eq "ko-KR") -or ($UserLocale -eq "ru-RU") -or ($UserLocale -eq "zh-CN") -or`
            ($PreviousLocale -eq "ja-JP") -or ($PreviousLocale -eq "ko-KR") -or ($PreviousLocale -eq "ru-RU") -or ($PreviousLocale -eq "zh-CN")) {
            Write-Host "Computer Locale has been changed, rebooting in 20 seconds"
            Sleep 20


Hopefully this is useful to someone, somewhere, sometime. Enjoy!





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 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)
                _engine = REngine.GetInstance(null, true, null, null);
                string rFilePath = ConfigurationManager.AppSettings["rFunctionPath"];
                _engine.Evaluate("source(\"" + rFilePath + "\")");
                GraphicsDevice = new SvgGraphicsDevice(new SvgContextMapper(400, 400, SvgUnitType.Pixel, null));

        public IHttpActionResult Execute(QueryData queryData)
            Stopwatch watch = new Stopwatch();
                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)
                    if (lastWidth != queryData.Width)
                        GraphicsDevice = new SvgGraphicsDevice(new SvgContextMapper(queryData.Width, queryData.Height, SvgUnitType.Pixel, null));
                        lastWidth = queryData.Width;

                    plots = GraphicsDevice.GetImages().Select(RenderSvg);
                return Ok(plots);

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



        private static string RenderSvg(SvgDocument image)
            using (var stream = new MemoryStream())
                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)

            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 + ")");

        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); = id; = id;

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

        scope.updateGraphic = function () {

            if (scope.config.ElementsList == undefined) {

            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++) {
            if (postData.Paths.length < 2) {
                alert('The symnol must have at least 2 attributes!');
            $'', postData).then(function (response) {
                scope.plots =;
                scope.plot = scope.plots[0];
                var currentElement = $document[0].getElementById(id)
                var currentElementWrappedID = angular.element(currentElement);
                scope.config.Height = scope.config.Width;
                scope.showGraphic = true;

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

        scope.unsafe = function (s) {
            return $sce.trustAsHtml(s);
        return { dataUpdate: scope.dataUpdate, resize: scope.resize };


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" />



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.




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!

Filter Blog

By date: By tag: