Skip navigation
All Places > PI Developers Club > Blog > 2018 > March
2018

PI AF 2017 R2 (AF SDK 2.9.5) was released shortly before 2018.  There are some exciting new features with AFSearch that should interest developers.

 

First off, I would hope any developer would always go to the Live Library What's New page for any major PI AF release.  That page gives a summary of what's new with that particular release, not just for AFSearch but for all namespaces.  Specific to AFSearch namespace, you would see the following:

 

OSIsoft.AF.Search Namespace

Two new query based search classes have been added in this release: AFAttributeSearch and AFNotificationContactTemplateSearch. The AFSearchToken.AFSearchToken(AFSearchFilter, AFSearchOperator, String, IList<AFSearchToken> ) constructor and the AFSearchToken.Tokens property have also been added to support enhanced nested query filters for searches.

The following new search tokens have been added: EventFrame, IsInternal, Parent, PlugIn, and PlugInName.

 

As a fellow developer and PI geek, when I first read that my reaction was "Cool! Nested queries!"  If you think about it, an AFAttributeSearch means that attributes are being searched upon some element(s) or event frame(s).  This implies there will first be a search for elements or event frames, followed by a search upon those results for the attributes.  This does not require you to create 2 search objects on the client.  Rather you will have 1 search object, namely an AFAttributeSearch, and it will have a nested query to filter on the desired elements or event frames.  But the nested queries are not limited to AFAttributeSearch.   For example, you may have an AFEventFrameSearch that uses a nested query for its elements.

 

Each release of AF SDK since 2.8.0 has introduced new features and capabilities to put more efficient searches at your fingertips.  For example, if you were searching on an attribute category to return many attributes per element, you could additionally filter on PlugIn or PlugInName to restrict the returned attributes to be PI points.

 

Contacts Search Example

 

Here's a quick example where I search for any contact with a name beginning with "Davi*".

 

using (AFNotificationContactTemplateSearch search = new AFNotificationContactTemplateSearch(assetServer, "contact demo", " Name:'Davi*' "))
{
    foreach (AFNotificationContactTemplate contactTemplate in search.FindNotificationContactTemplates(fullLoad: true))
    {
        Console.WriteLine($"   {contactTemplate.Name,-24} {contactTemplate.Contact}");
    }
}

 

The output:

 

   Template                 Contact

   David Burns_Email        David Burns

   David Doll_OCS           David Doll

   David Moler_Email        David Moler

 

 

Nested Queries

 

Let's take a look at 3 different examples that all do the exact same thing.  We want to perform an attribute search for any attributes named "Feed Rate" that belong to any elements whose name starts with "Boiler".  We will perform the same search 3 times, but each time how we setup the search object will be different.  The 3 techniques we will briefly cover are:

 

  • Using Nested Search Tokens
  • Using Nested Query String
  • Using Interpolated Nested Query String

 

They example code is kept simple.  We will perform an AFAttributeSearch searching for attributes found within a nested element query with the following filters:

  1. Attribute Category of "Process Monitoring"  (note the blank)
  2. Element Category of "ProcessMonitoring" (does not have a blank)
  3. Element Template of "FCC Pump Process"

 

If you've ever worked with AFSearch before, then your previous experience should have been that you could not specify Category twice on a query prior to nested queries in 2.9.5

 

Using Nested Search Tokens

 

string templateName = "FCC Pump Process";
string elemCatName = "ProcessMonitoring";
string attrCatName = "Process Monitoring";

List<AFSearchToken> nestedTokens = new List<AFSearchToken>();
nestedTokens.Add(new AFSearchToken(AFSearchFilter.Template, templateName));
nestedTokens.Add(new AFSearchToken(AFSearchFilter.Category, elemCatName));

List<AFSearchToken> tokens = new List<AFSearchToken>();
// The Element uses the nested token(s)
tokens.Add(new AFSearchToken(AFSearchFilter.Element, AFSearchOperator.Equal, null, nestedTokens));
// The Attribute uses the non-nested tokens, in this case just Category.
tokens.Add(new AFSearchToken(AFSearchFilter.Category, attrCatName));

using (AFAttributeSearch search = new AFAttributeSearch(root.Database, "nested tokens example", tokens))
{
    search.CacheTimeout = TimeSpan.FromMinutes(10);
    foreach (AFAttribute item in search.FindAttributes())
    {
        // Do something
    }
}

 

 

Using Nested Query String

 

The trick with a nested query string is that it will be enclosed in {braces}.

 

// Notice how element has nested { }. 
// The Category depends on nesting level. 'Process Monitoring' with a blank is outside the nesting,
// so it will be an Attribute Category, whereas 'ProcessMonitoring' inside the nesting is an
// Element Category.
string query = "Element:{Template:'FCC Pump Process' Category:'ProcessMonitoring'} Category:'Process Monitoring'";

using (AFAttributeSearch search = new AFAttributeSearch(root.Database, "nested query string", query))
{
    search.CacheTimeout = TimeSpan.FromMinutes(10);
    foreach (AFAttribute item in search.FindAttributes())
    {
        // Do Something
    }
}

 

 

Using Interpolated Nested Query Strings

 

With Interpolated Strings, you may easily substitute a variable's value (technically, it substitute's the string returned from the variable's ToString() method).  If you are familiar with this in either C# or VB.NET, you know that {braces} are used.  This raises an interesting question of how the Interpolated String knows which brace is for the nested query, and which is for the value substitution.  You would denote that using an escape sequence of the braces themselves.

 

string templateName = "FCC Pump Process";
string elemCatName = "ProcessMonitoring";
string attrCatName = "Process Monitoring";

// This gives a compile error with an Interpolated String
// string query = $"Element:{Template:'{templateName}' Category:'{elemCatName}'} Category:'{attrCatName}'";

// Escape the { and } around literal braces with {{ and }}.
// Fun Fact: {{something}} is called the Mustache Template!
// See https://en.wikipedia.org/wiki/Mustache_(template_system)
string query = $"Element:{{Template:'{templateName}' Category:'{elemCatName}'}} Category:'{attrCatName}'";

using (AFAttributeSearch search = new AFAttributeSearch(root.Database, "interpolated nested query string", query))
{
    search.CacheTimeout = TimeSpan.FromMinutes(10);
    foreach (AFAttribute item in search.FindAttributes())
    {
        // Do Something
    }
}

 

An interesting bit of trivia: the {{something}} format of double braces is called the Mustache Template!

 

 

AFAttributeSearch

 

The nested query examples used AFAttributeSearch for searching upon elements.  The AFAttributeSearch may search on event frames instead.

 

One thing to note is the fullLoad parameter is missing from the FindAttributes method because it must always do a full load since the attributes cannot exist without the owning element or event frame.  However, the AFAttributeSearch.FindObjectFields is smart enough to know when it must make a full load to evaluate data references.  To state that a different way, it is smart enough to know when to skip a full load because of captured event frames. You don't have to do anything special like consult Oracles to figure this out for any given situation.  You would code it the same way regardless of the situation and let AFAttributeSearch.FindObjectFields make the right decision!

 

Why you should use the new search classes

 

Maintainability - some Find methods in AFAttribute or AFNotificationContactTemplate have already been marked as Obsolete, and it is reasonable to suspect that others may be marked so with future releases of AF SDK.  Make your applications more resilient by switching to these new methods sooner rather than later.

 

Performance - if you require fetching more than one page of results from the server and you opt-in to server-side caching, any of the AFSearch classes perform much faster than the older methods.  This means that AFAttributeSearch will be much faster than the AFAttribute.FindElementAttributes overloads.

 

Ease of Use - the AFSearch classes take care of paging issues seamlessly for you.  The new nested queries makes searching for attributes on either elements or event frames very easy with fewer lines of code.  There is previously mentioned smartness built into AFAttributeSearch.FindObjectFields. All of this without extra coding on your part.

 

Reliability - when searching on attribute values that might be evaluated client-side (due to needing to evaluate the data reference), it is impossible to do paging properly using the older search methods because you don’t reliably know what to use for the next page index.  By opting-in to server-side caching with the newer AFSearch methods, paging is more reliable.

"IoT and Fog Computing: Develop Data Ingress Applications from Edge to Cloud" hands-on lab at PI World SF 2018:

 

Just one year ago, at the 2017 OSIsoft User Conference in San Francisco, we had long discussions and debates around the present and future of operational intelligence: thousands of new sensors and devices, petabytes

of data generated every day, fragmented in an endless number of incompatible protocols and interfaces. Furthermore, new and old players want to provide their own, isolated service, leaving developers and end users with

the [almost] insurmountable problem of putting all this stuff together and making it run in a smooth and cost-effective fashion.

That was the time when the idea of an Open Edge Module, that then became FogLAMP , was conceived.

 

One year later, at PIWorld 2018 in San Francisco, we are able to demonstrate the result of twelve months of hard coding on this concept. We have worked on a thriving open source project to bring any data coming

from new and old devices, sensors, actuators etc. into PI. This project is called FogLAMP (available on GitHub): free to learn, use, and adopt as every open source project is.

 

These are really exciting times, and for a good reason. PIWorld is a milestone for FogLAMP and a game changer for the whole Community involved in the development and implementation of IoT and IIoT projects.

For the first time, industrial-grade solutions can combine decades of investments in existing infrastructure with new and innovative technologies.

 

We will talk about this and more at the FogLAMP Community booth, where you will be able to meet developers and contributors from OSIsoft, Dianomic Systems, software providers and hardware manufacturers.

 

We also have two talks, one on the features of FogLAMP (with a short demo) and one on Fog Computing architectures.

 

North/South and East/West components interaction in a Fog Computing Architecture.

 

Certainly, the most important appointment for developers, the one that must be added to your agenda, is the IoT Lab titled “IoT and Fog Computing: Develop Data Ingress Applications from Edge to Cloud”.

The IoT and Fog Computing Lab

If you are a developer and you are interested in learning more about collecting data from sensors and devices and sending data to PI and Cloud systems, the IoT and Fog Computing Lab is for you.

 

The lab is packed with exercises that will help you understand and learn about problems and solutions associated to data collection, transformation, buffering and transfer. Put your hands on code and hardware that can

simulate typical scenarios in manufacturing, transportation, and in other industrial and infrastructure sectors.

 

But there is more! Participants will have access to a Raspberry PI Zero W and a set of sensors packed into a board called Enviro PHAT. We will place the boards and a battery pack on a remote controlled truck

to simulate an installation on moving devices, where you may experience intermittent connectivity with other layers of the data infrastructure.

 

Our Lab environment.

 

Last but not least, we will have a bit of fun with a game we have organized for you. We will run our RC trucks with the Raspberry PIs mounted on it, on a track where you can score points and race for the highest number of collected data in the room!

It will be fun to learn and play!

 

Don’t forget to register and bring your laptop. All you need to install is:

- A SSH client, such as PuTTY (Windows) or Terminal (MacOS)

- An API client, such as Postman (for Windows and MacOS)

- A Microsoft Remote Desktop client

 

The rest is on us for you to try, use and take home to learn even more!

 

Talks and Lab Schedule - Thursday, April 26, 2018:

- 10.30am - 11.15am: Introduction to the Open Edge Module (FogLAMP)

- 1.30pm - 4.30pm: IoT and Fog Computing: Develop Data Ingress Applications from Edge to Cloud

- 2.30pm - 3.15pm: Fog Computing and OSIsoft

 

Introduction

 

Although PI Vision 4 (2018) is not released yet, the participants of the Virtualization Virtual Hackathon 2018 need to learn the extensibility model of this product in order to raise their chances of creating a valuable custom symbol. As a result, I've decided to write the first blog post of the PI Vision 4 version of my "Developing the Google Maps custom symbol for PI Vision 3" blog post series.

 

The ultimate idea is that when the user drags an element and drops it on the PI Vision display, a Google Map will be created with a marker located according to the values of the latitude and longitude attributes of the dropped element. If another element is dropped on the map, another marker should be created accordingly.

 

This blog post (part 1) will focus on creating the map only, which is not something trivial.

 

 

Disclaimer

 

Again, PI Vision 4 is not released yet. The hackathon participants are working with a preview version. Therefore, this library might not be compatible with the released version of PI Vision 4. I will update this article and library as soon as PI Vision 4 is released though.

PI Vision 4 public preview is planned to start at PI World San Francisco 2018. You will have the opportunity to test yourself this new extensibility model! The PI Vision 4 release is planned for Q4 2018.

 

Setting up your environment

 

I will comment this topic in details after PI Vision 4 is released. For now, the hackathon participants will access their Virtual Machine with the environment already set up. In order to develop your custom symbol, the following products are used:

  • Visual Studio Code
  • Google Chrome
  • Node.js/npm
  • Git

 

If you take a look at the Virtual Machine, you will realize that the PI Vision Extension Library Seed Project was already cloned to the C:\src\pi-vision-extensions folder.

 

Getting started developing the PI Vision symbol

 

Open the command prompt and navigate to the C:\src\pi-vision-extensions folder. Then type "code .". Visual Studio Code with the PI Vision Extensions project will be opened.

 

First rename the \src\example folder and its files. They should start with gmaps instead of example as shown on the screenshot below.

 

 

 

Don't worry, the gmaps-loader.service.ts will be created later. The code to get started for the gmaps.component.ts is below:

 

import { Component, OnChanges, OnInit } from '@angular/core';


@Component({
  selector: 'gmaps',
  templateUrl: 'gmaps.component.html',
  styleUrls: ['gmaps.component.css']
})
export class GoogleMapsComponent implements OnInit, OnChanges {
  constructor()
  {
  }


  ngOnInit() {
  } 
  
  ngOnChanges(changes) {
    if (changes.data) {
     
    }
  }
  
}

 

The custom symbol is actually an Angular component whose decorator (@Component) is its metadata. It describes the selector, templateUrl and styleUrls. You can find more information about Angular in its official web site.

 

The content of HTML template for this component (gmaps.component.html) file is:

 

<div #gmap style="width:100%;height:100%"></div>

 

In order to create a map you just need a div HTML node. The rest is handled by JavaScript.

 

The next step is to write the Google Maps JavaScript code. Since we are using TypeScript, the definition of the Google Maps classes of needs to be downloaded and installed through the command below:

 

npm install --save @types/googlemaps

 

 

 

Getting started with Google Maps JavaScript API

 

Google provides a programming reference and samples for Google Maps JavaScript API, which were really useful to write this blog post. Let's take a look at the most basic example. Their HTML page has the following source code:

 

<!DOCTYPE html>
<html>
  <head>
    <title>Simple Map</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <style>
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
      #map {
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>

var map;
function initMap() {
  map = new google.maps.Map(document.getElementById('map'), {
    center: {lat: -34.397, lng: 150.644},
    zoom: 8
  });
}

    </script>
    <script src="https://maps.googleapis.com/maps/api/js?&callback=initMap" async defer></script>
  </body>
</html>

 

 

Ok, we have some problems to solve:

 

  1. Editing the index file from the PI Vision 4 web site in order to load the Google Maps JavaScript API is not a recommended practice. The symbol will have to do this task.
  2. The second problem is that url which refers to the GMaps (Google Maps) library has the name of the callback function to be called after this library is loaded. How can we make this work within Angular 5?
  3. Users can add as many instances of this symbol as they want. On the other hand, the GMaps libraries needs to be loaded only once. How to make sure there won't be any conflict?

 

 

Solving problem 1: After some research, I found this interesting StackOverflow page, which allows us to dynamically load external JavaScript scripts using Typescript. After making some changes, here is the code that makes the trick:

 

const url = "https://maps.google.com/maps/api/js?key=AIzaSyDUQhTeNplK37EX-mXdAB-zVuYDutE5c2w&callback=gMapsCallback"
let node = document.createElement('script');
node.src = url;
node.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(node);

 

 

Solving problem 2 and 3: If we define a function as property of the window JavaScript object, GMaps will be able to call it. Therefore, we have defined the window['gMapsCallback'] function as:

 

GoogleMapsLoader.promise = new Promise( resolve => {
    
    // Set callback for when google maps is loaded.
    window['gMapsCallback'] = (ev) => {
         resolve();
    };


    let node = document.createElement('script');
    node.src = url;
    node.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(node);
});

 

We have created a JavaScript Promise that when it is solved it will define the window['gMapsCallback'] and then call the Google Maps JavaScript API which will call the window['gMapsCallback'] method. The beauty of this approach is that the promise is resolved only once which means that the Google Maps will be loaded also once.

 

An Angular service named GoogleMapsLoader is created to load the Google Maps JavaScript library with the following code snippet:

 

 

import { Injectable } from '@angular/core';


const url = "https://maps.google.com/maps/api/js?key=AIzaSyDUQhTeNplK37EX-mXdAB-zVuYDutE5c2w&callback=gMapsCallback"
@Injectable()
export class GoogleMapsLoader {
  private static promise;
  public static load() {
      // First time 'load' is called?
      if (!GoogleMapsLoader.promise) {
          // Make promise to load
          GoogleMapsLoader.promise = new Promise( resolve => {
              // Set callback for when google maps is loaded.
              window['gMapsCallback'] = (ev) => {
                  resolve();
              };


              let node = document.createElement('script');
              node.src = url;
              node.type = 'text/javascript';
              document.getElementsByTagName('head')[0].appendChild(node);
          });
      }
      // Always return promise. When 'load' is called many times, the promise is already resolved.
      return GoogleMapsLoader.promise;
  }
}

 

On the custom symbol (Angular component), this is how you would load the library by calling the GoolgeMapsLoader service:

 

      GoogleMapsLoader.load().then(res => {


      });

 

 

With all these concepts and restrictions in mind, here is the final version of this blog post (part 1).

 

import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
import { GoogleMapsLoader } from './gmaps-loader.service'


@Component({
  selector: 'gmaps',
  templateUrl: 'gmaps.component.html',
  styleUrls: ['gmaps.component.css']
})


export class GoogleMapsComponent implements OnInit, OnChanges {
  @ViewChild('gmap') gmapElement: any;
  private map : google.maps.Map


  constructor(private mapLoader : GoogleMapsLoader)
  {


  }


  ngOnInit() {


    GoogleMapsLoader.load().then(res => {
        console.log('GoogleMapsLoader.load.then', res);
        this.initMap();
    });
  } 


  ngOnChanges(changes) {
    if (changes.data) {
     
    }
  }


  private initMap() {
    var mapProp = {
      center: new google.maps.LatLng(18.5793, 73.8143),
      zoom: 15,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    };


    this.map = new google.maps.Map(this.gmapElement.nativeElement, mapProp);
  }
}

 

 

When the custom symbol is created, the ngOnInit() method is called. This function calls initMap() which creates a map with a specific center and zoom. The ngOnChanges() method is called whenever the PI System receives a new value for the associated attribute or element.

The focus of this blog post is just to create a map for each custom symbol added to the display. This method will be used on the following parts of this blog post series.

 

The last step is to update the module.ts on the root folder with the following information:

Rename the ExampleComponent to GoogleMapsComponent which is present on the declaration, exports and entryComponents fields  of the NgModule declarator. Add the GoogleMapsLoader to the providers field which should contain all the services. In order to be successful you need to import those modules properly.

 

Finally, we need to rename the symbol properties on the ExtensionLibrary class. We have copied the google-maps.svg from this GitHub repository and pasted into the \src\assets\images folder. We have deleted all the items from the configProps array of the generalConfig object as at this point we are not interested in setting up the configuration pane of the symbol.

 

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgLibrary, SymbolType, SymbolInputType, ConfigPropType } from './framework';
import { LibModuleNgFactory } from './module.ngfactory';
import {GoogleMapsLoader} from './gmaps/gmaps-loader.service'
import { GoogleMapsComponent} from './gmaps/gmaps.component';


@NgModule({
  declarations: [ GoogleMapsComponent ],
  imports: [ CommonModule ] ,
  providers:  [GoogleMapsLoader],
  exports: [ GoogleMapsComponent],
  entryComponents: [ GoogleMapsComponent ]
})
export class LibModule { }


export class ExtensionLibrary extends NgLibrary {
  module = LibModule;
  moduleFactory = LibModuleNgFactory;
  symbols: SymbolType[] = [
    {
      name: 'gmaps-symbol',
      displayName: 'Google Maps Symbol',
      dataParams: { shape: 'single' },
      thumbnail: '^/assets/images/google-maps.svg',
      compCtor: GoogleMapsComponent,
      inputs: [
        SymbolInputType.Data,
        SymbolInputType.PathPrefix
      ],
      generalConfig: [
        {
          name: 'Google Maps Options',
          isExpanded: true,
          configProps: [ ]
        }
      ],
      layoutWidth: 200,
      layoutHeight: 200
    }
  ];
}

 

Save all files, run your local web pack server using "npm start run" and open PI Vision 4 using Google Chrome. In order to load the custom symbols, you need to go to the PI Vision landing page, select Options on the left pane and turn on the developer mode, according to the screenshot below:

 

 

 

Create a new display. Click on the Google Maps symbol on the left pane. Each click will create a new instance of the Google Maps custom symbol and add it to the PI Vision display. Make sure not only that the Google Maps symbol is added to the top-left pane but also that no exception is thrown when multiple symbols are added on a single display (please check the Google Chrome developer tools).

 

 

Conclusions

If you are a hackathon participant reading this blog post, I hope this material will help you create valueable custom symbols. If this is not the case, I hope you will have a good idea about the new extensibility model of PI Vision 4 and Angular 5.

PI World Innovation Hackathon 2018 

When: April 23th 9am (Day 0) - April 24th 8am (Day 1) 

Where: Embarcadero Room – Parc 55 Hotel

 

Join us at the Innovation Hackathon happening at PI World San Francisco 2018 to learn, network, and compete for prizes! Whether you are a PI System Developer, Architect, Integrator, Administrator, Business Analyst, or Data Scientist, you can enjoy this high-energy and vibrant event. OSIsoft will provide all of the tools, subject matter experts, and a ready-to-start environment. Just show up with your ideas and energy to develop a killer app in 23 hours! 

 

 

The data sponsor for the PI World 2018 SF Innovation Hackathon is DCP Midstream, which is one of the largest producers of natural gas liquids and one of the largest natural gas processing companies in the U.S. They gather and/or process about 12 percent of our nation’s gas supply.

 

The winners of this event will receive the following prizes:

1st place:

  • One-time 100% discount to attend the UC per team member*
  • Free 1-year subscription to PI Developers Club
  • Featured in PI Developers Club Community
  • Echo Dot + Sonos PLAY 5: Ultimate Wireless Smart Speaker for Streaming Music.

2nd place:

  • One-time 50% discount to attend the UC per team member*
  • Free 1-year subscription to PI Developers Club
  • Featured in PI Developers Club Community
  • Bose Quiet Comfort 35 (Series II)

3rd place:

  • Free 1-year subscription to PI Developers Club
  • Featured in PI Developers Club Community
  • Vilros Raspberry Pi 3 Retro Arcade Gaming Kit + 5 USB Classic Controllers

 

*Note: Discounts are for any OSIsoft PI World events in 2018 or 2019. TechCon labs and other training sessions are not included.

 

 

Register Today

 

Disclaimer: “You understand that OSIsoft strives to conduct business according to the highest ethical standards, and it may determine in its sole discretion that it is not appropriate to provide a prize to a winner under certain circumstances.”

The webinar scheduled for today (Wednesday March 21) will begin at 4:00 PM GMT or UTC.  Due to an unfortunate glitch with North America observing DST and Europe not, the previously published start times are incorrect.  Europe does not transition to Summer Time until March 25.

 

Recording and Slides are available at this link.

 

Topic: Asset Analytics native integration with MATLAB

 

North America 4:00 PM GMT is:

     Noon - 1 PM US Eastern

     9-10 AM US Pacific

 

Europe 4:00 PM GMT is:

     5:00 PM CET

 

UPDATE: A customer reported that the link in his confirmation mail 'click here to join' does not work, because it is missing the ':' after https. We don't know how widespread this is or whether it is a one-off for this particular customer.  However, he did report that the the URL in the Outlook invitation/appoint is OK.

Table of Contents

 

 

Many of the .NET based code samples on PI Square and GitHub are in C#.  It’s easy for a VB.NET developer to feel left out.  If you’ve been following me within the past 5 years you may think that I am a heavy duty C# developer.  Well, I am actually, and C# is definitely my first choice for writing applications.  However, I do have a fondness for VB as I started out with Visual Basic 3 in the mid-1990’s and was able to eek out a nice living writing for VB, VBA, and eventually VB.NET.  VB helped feed and clothe my family for close to 2 decades.

 

Coming from VB 6, VB.NET was a natural introduction to .NET programming.  But early on, I started to wean myself away from VB-centric calls and more towards .NET-centric ones.  For example, I replaced VB’s MsgBox calls with .NET’s MessageBox.  This transitioned me to the point where I stopped calling myself a VB developer and instead started calling myself a .NET developer because it was the .NET Framework, not the language used, that really was at the heart of my applications.  After a while of doing that, I found it an easier transition to switch from VB.NET to C# than when I had switched from VB 6 to VB.NET.

 

There is no need to put your "VB Forever" shields up.  I am not here to convince you to switch to C#.  You are more than welcome to stay with VB.NET.  What I am here to do is to help explain a few language specific things about C# so that it makes it easier for VB.NET coders like yourself to follow the C# examples more easily and without being stumped by some C# idiosyncrasies.  To a large degree, many of our examples are fairly easy to follow.  Putting aside the unsightly semi-colons and multitude of braces versus the wordiness of Visual Basic, both languages obviously have an overlapped feature set.  It’s easy to understand that C#'s foreach and VB's For Each do the same thing.  Likewise, both languages use similar features like Interpolated Strings, which look quite similar in either language.

 

This cheat sheet is to help address those few C# things that aren’t immediately translatable to VB.  That way you can begin to put your focus back on PI Developer Technologies and not worry so much about a language that is foreign to you.

 

 

Logical Operators

Earlier versions of VB did not have short-circuiting but VB.NET has had it for quite a long while.

C#
VB.NET
===
&And
&&AndAlso
|Or
||OrElse

 

 

var keyword for implicitly typed variables

Many C# snippets may occasionally employ the var keyword to declare variables.  For someone unfamiliar with it, they may mistakenly believe that the variable has a type of Object and may take any value of any data type (like Variant in the olden days).  This is not true. With var, a variable is assigned the type from the right-hand expression, and once that type is assigned it cannot be changed.  Microsoft's own recommendation is to use var when (1) the data type on the right-hand side is easily understood, or (2) if you really don't care about the type for variable with a very short scope.

 

var thing1 = "Hello, World!";   // always a string
var thing2 = 1;                   // always an Int32
var thing3 = 1.0;                // always a Double
var thing4 = 1.0F;                 // always a Single

 

The equivalent code in VB.NET would be:

 

Dim thing1 = “Hello, World!”;  ' always a string
Dim thing2 = 1;  ' always an Int32
Dim thing3 = 1.0; ' always a Double
Dim thing4 = 1.0F; ' always a Single

 

 

Conditional Operator, or ? : operator

You may frequently see statements such as this peppered in C# code:

 

var lastIndex = (list != null) ? list.Count – 1 : -1;

 

That is to say condition ? truePart : falsePart;  This is called the conditional operator.  The equivalent in VB.NET would use the If operator:

 

Dim lastIndex = If(list != null, list.Count – 1, -1)

 

One should emphatically NOT use the IIf function, as it has the adverse side-effect of evaluating both the TruePart and FalsePart regardless of the condition.  For example:

 

IIf(condition, SomeFunction1(), SomeFunction2())

 

Is the same as this code:

 

Dim truePart = SomeFunction1()      ' always runs regardless of condition
Dim falsePart = SomeFunction2()     ' always runs regardless of condition 
If condition Then
    truePart
Else
    falsePart
End If

 

Whereas the If operator is equivalent to this bit of code:

 

If condition Then
    ' will only run if condition is True
    SomeFunction1()
Else
    ' will only run if condition is False
    SomeFunction2()
End If

 

 

Null-Coalescing Operator, or ??

I personally am not a big fan of the null-coalescing operator or simply ??.  It returns the left-hand operand if that operand is not null; otherwise it returns the right hand operand.  The VB equivalent is also the If operator but with only 2 arguments, and the first argument must be a nullable reference type.

 

There is a decent explanation of how it works with VB.NET in this StackOverflow link.  See the answer by Code Maverick on Dec 19, 2013.

 

 

Read-only Auto-properties (or get-only property)

C# version 6.0 introduced read-only auto-properties, which might seem odd to ponder a get-only property:

 

Public property AFDatabase Database { get; } 

 

The above is equivalent to this in earlier versions of C#:

 

Private readonly AFDatabase _database;
Public AFDatabase Database { get { return _database; } }

 

In either case, the readonly property may only be assigned a value at class initialization or else within a class constructor.

 

The equivalent in VB.NET would be:

 

    Private ReadOnly _database As AFDatabase
    Public ReadOnly Property Database() As AFDatabase
        Get
            Return _database
        End Get
    End Property

 

Note that in VB both the property - and the private backing field - must be decorated as ReadOnly.

 

 

Expression-bodied members (property => expression)

A C# expression-bodied member may work for methods as well as properties, and are best reserved for a quick one-line piece of code.  Borrowing from a snippet above, an example would be:

 

Public AFDatabase Database => _database; 

 

Which is the equivalent of:

 

Public AFDatabase Database { get { return _database; } }

 

However, VB.NET does not support expression-bodied members so you would have to use the more verbose VB code referenced in the previous VB example.

 

 

Remainder or Modulus operator

In VB, the Mod operator is used to find the remainder:

 

Dim quartile As Integer = value Mod 4

 

In C#, the % operator does the same thing:

 

int quartile = value % 4;

 

 

Integer versus Floating Point Division

In VB, the / operator will always perform floating point division, even if both operands are integers.  To perform integer division with VB, one would use the \ operator.

 

Dim a = 5 / 2      ' a is a Double = 2.5
Dim b = 5 \ 2      ' b is an Int32 = 2

 

In C#, division on 2 integers always results in integer division.  There is no direct equivalent to VB's \ operator.  In order for floating point division to occur in C#, at least one of the operands should be a floating point value.

 

var a = 5 / 2;     // a is an Int32 = 2 since 5 and 2 are Int32
var b = 5 / 2.0;     // b is a double = 2.5 since 2.0 is a double
var c = (double)5 / 2;     // c is a double = 2.5 since integer 5 is cast to double before division

 

 

Related Links

For additional reading, you may want to check out this comparison of language features between C# 6.0 and VB 14.

 

There you go.  We've given a little love to our VB.NET community, which is probably larger than we think it is (you are certainly a quiet bunch).  You may let me know if you liked this in comments below.  Or you may comment if there are any C# translations you would like to see covered.  I will be glad to add it to this page.  As stated in the introduction, my intent is to empower you to read C# examples so that you are better equipped to translate to VB.

Hello everybody,

 

I would like to share with you an app I've been working on. I call it AF Bash. It allows you to interact with AF the same way you interact with your files through CMD:

2018-03-13 14_48_38-C__Users_rborges_Documents_Projects_afbash_afbash_bin_Debug_afbash.exe.png

 

From a code perspective, it's a framework that provides a quick and easy way for you to implement your set of commands, so I encourage you to write your custom commands and send a pull request to the main repository! But first, lets break it down into topics and explain the architecture and implementation details

 

1) Architecture

Here is a very simple implementation diagram:

2018-03-13 16_12_05-Drawing1 - Visio Professional.png

 

AFBash is a console application that uses Autofac as its IoC container and exposes an interface called ICommand that is implemented by BaseCommand, an abstract class that is the base class for all commands to derive from. It provides a context class full of goodies that should be used to access AF SDK data. The console is wrapped around a custom version of ReadLine, where I can manage command history, autocompletation and command cancelling.

 

I strongly encourage you to follow the comments in the main entry point because it will make easier o understand how the main loop works and what it expects from your custom Command.

 

2) Adding a new Command

So lets stop the chit chat and go to the fun part. How to create a custom command. For this example, let me show you how to implement a dir / ls command.

 

First you need a class that is derived from BaseCommand.

 

class Dir : BaseCommand 

 

Because we are using Autofac's IoC container, we just need to declare a ctor that receives a Context as parameter.

 

public Dir(Context context) : base(context)

 

Now we have to take care of 4 functions:

 

public override ConsoleOutput Execute(CancellationToken token)
public override List<string> GetAliases()
public override string GetHelp()
public override (bool, string) ProcessArgs(string args)

 

The GetAliases() function must provide the alias you want for the command that you are implementing. The GetHelp() must return a simple help information.

 

public override List<string> GetAliases()
{
     return new List<string> { "dir", "ls" };
}
public override string GetHelp()
{
     return "Lists all children of an element";
}

 

The ProcessArgs() function is where get the arguments that your command must parse and store any variable that will be used later by Execute(). Note that BaseCommand exposes a global variable called BaseElement where you can store the AFObject output of your parsing. Going get back to our exemple, a dir command can be execute with or without parameters. A parameter-less dir implies that you want to list everything from the current element. Meanwhile, a parameter may be a full or relative path. So how to take care of it? simple. The AppContext has a function called GetElementRelativeToCurrent(string arg). It will return the element based on the argument passed. Here some examples:

Current
Argument
Result
\\Server\Database\FirstElement\Child".."\\Server\Database\FirstElement
\\Server\Database\FirstElement\Child"grandChild"\\Server\Database\FirstElement\Child\grandChild
\\Server\Database\FirstElement\Child"\" or "\\"\\ (a state where no element is selected)
\\Server\Database\FirstElement\Child"~"\\DefaultServer\DefaultDatabase
\\Server\Database\FirstElement\"child\grandChild"\\Server\Database\FirstElement\Child\grandChild
\\Server\Database\FirstElement\Child"" or null\\Server\Database\FirstElement\Child

 

So far we have this for our argument processing (note that I'm using C#7.0, where a function can return multiple arguments.):

 

public virtual (bool, string) ProcessArgs(string args)
{
    BaseElement = AppContext.GetElementRelativeToCurrent(args);

    if (BaseElement == null)
        return (false, string.Format("Object '{0}' not found", args));
    else
        return (true, null);
}

 

It's only missing one thing: when your current element is the top most node, the CurrentElement is Null because there is no PISystem selected. So a Null BaseElement is not necessarily a bad thing. We just have to check whether the user intentionally did that or was a mistake. This processing is already implemented as a virtual function on BaseCommand. So if your function argument is a path you don't even need to override it as BaseElement will be populated with the target element.

 

Finally the Execute() function.

 

It must return a ConsoleOutput, a wrapper class that makes easier for you to print structures into the console. In our exemple lets set a header message like CMD's dir:

 

ConsoleOutput console = new ConsoleOutput();
console.AddHeaderLine(string.Format("Children of {0}", BaseElement is null ? "root" : BaseElement.GetPath()));

 

We have a table-like result, so we need to set the headers:

 

console.SetBodyHeader(new List<string> { "Type", "Name" });

 

Now, we get the children of BaseElement and loop through them, printing everything we want:

 

var children = AppContext.GetChildren(BaseElement);
children.ForEach(c => {
                console.AddBodyLine(new List<Tuple<string, Color>> {
                    new Tuple<string, Color>(c.Identity.ToString(), AppContext.Colors[c.Identity]),
                    new Tuple<string, Color>(c.ToString(), AppContext.Colors.Base)
                });
            });
return console;

 

And that's it! Now you just need to compile and you are good to go! The actual implementation of my dir also handle data for attributes. I encourage you to go and see how I did it.

 

3) Available Commands

 

So far I have implemented 7 basic commands: CD / LS

 

This is a work in progress, so if you clone this project, keep your repo up-to-date because I will keep pushing bug fixes and new features.

 

Finally, if you find bugs or have questions, let me know!

Introduction

 

I've recently published a blog post about generating the WebID on the client with the PI Web API client library for .NET Standard. It explains how the user can access the webIdHelper class in order to generate a WebID 2.0 on the client, without having to make an HTTP request. This class with the same methods and logic were added to the PI Web API client libraries for jQuery, Angular and AngularJS. This updated client library for AngularJS can also be used in PI Vision 3. As a result, the WebID 2.0 client generation feature is available when developing a custom PI Vision 3 symbol.

 

Note that those methods will only work if you are using PI Web API 2017 R2+.

 

Example in JavaScript (jQuery and AngularJS)

 

    var point1webId = piwebapi.webIdHelper.generateWebIdByPath("\\\\MARC-PI2016\\SINUSOID", "PIPoint");
    var point2webId = piwebapi.webIdHelper.generateWebIdByPath("\\\\MARC-PI2016\\CDT158", "PIPoint");
    var point3webId = piwebapi.webIdHelper.generateWebIdByPath("\\\\MARC-PI2016\\SINUSOIDU", "PIPoint");
    var piAttributeWebId = piwebapi.webIdHelper.generateWebIdByPath("\\\\MARC-PI2016\\CrossPlatformLab\\marc.adm|Heading", "PIAttribute", "PIElement");
    var piElementWebId = piwebapi.webIdHelper.generateWebIdByPath("\\\\MARC-PI2016\\CrossPlatformLab\\marc.adm", "PIElement");
    var piDataServerWebId = piwebapi.webIdHelper.generateWebIdByPath("\\\\MARC-PI2016", "PIDataServer");


    var piDataServer = null;
    var piAttribute = null;
    var piElement = null;
    piwebapi.dataServer.get(piDataServerWebId).then(function (response) {
        piDataServer = response.data;
    });
    piwebapi.attribute.get(piAttributeWebId).then(function (response) {
        piAttribute = response.data;
    });
    piwebapi.element.get(piElementWebId).then(function (response) {
        piElement = response.data;
    });




    var piAttributeWebIdInfo = piwebapi.webIdHelper.getWebIdInfo(piAttributeWebId);
    var piElementWebIdInfo = piwebapi.webIdHelper.getWebIdInfo(piElementWebId);
    var piDataServerWebIdInfo = piwebapi.webIdHelper.getWebIdInfo(piDataServerWebId); 

 

 

 

Example in TypeScript (Angular)

 

    let point1webId = this.piWebApiHttpService.webIdHelper.generateWebIdByPath("\\\\PISRV1\\SINUSOID", PIPoint.name, null);
    let point2webId = this.piWebApiHttpService.webIdHelper.generateWebIdByPath("\\\\PISRV1\\CDT158", PIPoint.name, null);
    let point3webId = this.piWebApiHttpService.webIdHelper.generateWebIdByPath("\\\\PISRV1\\SINUSOIDU", PIPoint.name, null);
    let piAttributeWebId = this.piWebApiHttpService.webIdHelper.generateWebIdByPath("\\\\PISRV1\\Universities\\UC Davis\\Buildings\\Academic Surge Building|Electricity Totalizer", PIAttribute.name, PIElement.name);
    let piElementWebId = this.piWebApiHttpService.webIdHelper.generateWebIdByPath("\\\\PISRV1\\Universities\\UC Davis\\Buildings\\Academic Surge Building", PIElement.name, null);
    let piDataServerWebId = this.piWebApiHttpService.webIdHelper.generateWebIdByPath("\\\\PISRV1", PIDataServer.name, null);


    let piDataServer: PIDataServer = null;
    let piAttribute: PIAttribute = null;
    let piElement: PIElement = null;
    this.piWebApiHttpService.dataServer.get(piDataServerWebId).subscribe(res => {
        piDataServer = res;
    });
    this.piWebApiHttpService.attribute.get(piAttributeWebId).subscribe(res => {
        piAttribute = res;
    });
    this.piWebApiHttpService.element.get(piElementWebId).subscribe(res => {
        piElement = res;
    });




    let piAttributeWebIdInfo = this.piWebApiHttpService.webIdHelper.getWebIdInfo(piAttributeWebId);
    let piElementWebIdInfo = this.piWebApiHttpService.webIdHelper.getWebIdInfo(piElementWebId);
    let piDataServerWebIdInfo = this.piWebApiHttpService.webIdHelper.getWebIdInfo(piDataServerWebId);  

 

 

The workflow of both examples above is:

  • Use the generateWebIdByPath method from the webIdHelper class to generate the WebID 2.0 of a PI object. You need to define the type of the object on the second input of this function. According to the PI object, the third input might be necessary to define the owner's type.
  • By calling the Get() method from Attribute, Element and DataServer controllers and reviewing the response, we will make sure that PI Web API understands the generated WebIDs. Otherwise, it will return an exception or an error status code in their response.
  • Finally, we call the getWebIdInfo() method from the webIdHelper class to get useful information about the WebID such as Path, ObjectID, ServerID, WebID Version and WebIDType.

 

You can learn more about those methods and the logic in the background in the .NET version of this blog post.

 

Example in a PI Vision 3 custom symbol

 

In this example we are creating a custom symbol on top of the PI Web API client library for AngularJS according to this blog post. This symbol gets the path of the element which is used to generate the WebID. Then the symbol calls the getSummary() method using the generated WebID from the Calculation controller. This method is returns the result of evaluating the expression over the time range. The JSON response is shown directly on the screen since our goal is to show the WebID generation.

 

Do not forget to update the angular-piwebapi-kerberos.js to version 1.1.1.

 

Please refer to the full code snippet below:

 

 

function loadPIWebApiModule() {
    var app = angular.module(APPNAME);
    var hasPiWebApiLoaded = (app.requires.indexOf("ngPIWebApi") > -1);


    if (hasPiWebApiLoaded == false) {


        var xhrObj = new XMLHttpRequest();
        xhrObj.open('GET', "/PIVision/Scripts/app/editor/symbols/ext/libraries/angular-piwebapi-kerberos.min.js", false);
        xhrObj.send('');
        var se = document.createElement('script');
        se.type = "text/javascript";
        se.text = xhrObj.responseText;
        document.getElementsByTagName('head')[0].appendChild(se);
        piWebApiApp = angular.module('ngPIWebApi');
        var app = angular.module(APPNAME);
        app.requires.push('ngPIWebApi');
    }
}




(function (PV) {
    function symbolVis() { }


    loadPIWebApiModule();
    PV.deriveVisualizationFromBase(symbolVis);


    var definition = {
        typeName: 'piwebapi-adv-sample',
        displayName: 'PI Web API Client library advanced sample',
        datasourceBehavior: PV.Extensibility.Enums.DatasourceBehaviors.Multiple,
        inject: ['piwebapi'],
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 400,
                Width: 400,
                MarkerColor: 'rgb(255,0,0)'
            };
        },
        visObjectType: symbolVis
    };




    symbolVis.prototype.init = function init(scope, elem, piwebapi) {
        piwebapi.ConfigureInstance("https://marc-web-sql.marc.net/piwebapi", true);




        this.onDataUpdate = dataUpdate;
        this.onConfigChange = configChanged;
        this.onResize = resize;






        function configChanged(config, oldConfig) {
            console.log('configChange called');
        };


        function resize(width, height) {
            console.log('resized called');
        }


        scope.streamWebId = null;


        function dataUpdate(data) {
            if ((data == null) || (data.Rows.length == 0)) {
                return;
            }


            var firstDataRow = data.Rows[0];
            if (firstDataRow.Path) {
                var path = firstDataRow.Path.substring(3, firstDataRow.Path.length);
                if (firstDataRow.Path.substring(0, 2) == "pi") {
                    path = "\\\\" + path.split("\\")[2]
                    scope.streamWebId = piwebapi.webIdHelper.generateWebIdByPath(path, "PIDataServer");
                }
                if (firstDataRow.Path.substring(0, 2) == "af") {
                    path = path.split("|")[0];
                    scope.streamWebId = piwebapi.webIdHelper.generateWebIdByPath(path, "PIElement");
                }


            }


            if (scope.streamWebId != null) {


                calculationBasis = 'TimeWeighted';
                endTime = "*";
                expression = "2*'Latitude'";
                sampleInterval = undefined;
                sampleType = undefined;
                selectedFields = undefined;
                startTime = "*-1d";
                summaryDuration = undefined;
                summaryType = "total";
                timeType = undefined;
                webId = scope.streamWebId;
                piwebapi.calculation.getSummary(calculationBasis, endTime, expression, sampleInterval, sampleType, selectedFields, startTime, summaryDuration, summaryType, timeType, webId, null).then(function (response) {
                    scope.data = response.data;
                });
            }
        }
    }
    PV.symbolCatalog.register(definition);
})(window.PIVisualization);

 

 

 

 

Conclusions

 

Generating the Web ID 2.0 on the client provides a great performance improvement on your web application and PI Vision 3 custom symbols. Use this feature whenever is possible making sure your PI Web API version is compatible with this great feature.

 

Please share your comments and thoughts below!

Introduction

 

I have already published a blog post about using PI Web API client library for AngularJS in PI Vision 2016 R2 and PI Vision 2017. Nevertheless, it was needed to modify the PI Vision source code, which was an important factor for our customers and partners not to use this great feature.

 

On this blog post, I will show you a new way to use PI Web API client library for AngularJS without changing the PI Vision 3 source code. This approach was tested in PI Vision 2017 and PI Vision 2017 R2.

 

Creating a custom symbol using the piwebapi service

 

First of all, open the browser and go to this GitHub repository. Download the source code package to a zip file. Within this file, copy the \dist\angular-piwebapi-kerberos.min.js file to %PIHOME64%\PIVision\Scripts\app\editor folder.

 

Since the PI Vision 3 source code won't be modified, we need to find a new way to:

  1. Detect if the library was already loaded
  2. If not, load the angular-piwebapi-kerberos.min.js script.
  3. Add the ngPIWebApi as a dependency of PI Vision.

 

As a result, all custom libraries using the PI Web API client library needs to start with the method below:

 

function loadPIWebApiModule() {
    var app = angular.module(APPNAME);
    var hasPiWebApiLoaded = (app.requires.indexOf("ngPIWebApi") > -1);


    if (hasPiWebApiLoaded == false) {
        var xhrObj = new XMLHttpRequest();
        xhrObj.open('GET', "/PIVision/Scripts/app/editor/symbols/ext/libraries/angular-piwebapi-kerberos.min.js", false);
        xhrObj.send('');
        var se = document.createElement('script');
        se.type = "text/javascript";
        se.text = xhrObj.responseText;
        document.getElementsByTagName('head')[0].appendChild(se);
        piWebApiApp = angular.module('ngPIWebApi');
        var app = angular.module(APPNAME);
        app.requires.push('ngPIWebApi');
    }
}

 

 

As soon as the custom library starts to load, it needs to call loadPIWebApiModule() method. The reason for that is we want to make sure that the script (angular-piwebapi-kerberos.min.js) is loaded before the symbolVis.prototype.init() method is called as the third input of this method is the PI Web API service (piwebapi Angular service variable). Let's take a look at the example provide in my previous blog post.

 

Create a new file name sym-piwebapi-basic-sample.js on the %PIHOME64%\PIVision\Scripts\app\editor\symbols\ext folder with the following content:

 

function loadPIWebApiModule() {
    var app = angular.module(APPNAME);
    var hasPiWebApiLoaded = (app.requires.indexOf("ngPIWebApi") > -1);


    if (hasPiWebApiLoaded == false) {
        var xhrObj = new XMLHttpRequest();
        xhrObj.open('GET', "/PIVision/Scripts/app/editor/symbols/ext/libraries/angular-piwebapi-kerberos.min.js", false);
        xhrObj.send('');
        var se = document.createElement('script');
        se.type = "text/javascript";
        se.text = xhrObj.responseText;
        document.getElementsByTagName('head')[0].appendChild(se);
        piWebApiApp = angular.module('ngPIWebApi');
        var app = angular.module(APPNAME);
        app.requires.push('ngPIWebApi');
    }
}




(function (PV) {
    function symbolVis() { }


    loadPIWebApiModule();
    PV.deriveVisualizationFromBase(symbolVis);


    var definition = {
        typeName: 'piwebapi-basic-sample',
        displayName: 'PI Web API Client library basic sample',
        datasourceBehavior: PV.Extensibility.Enums.DatasourceBehaviors.Multiple,
        inject: ['piwebapi'],
        getDefaultConfig: function () {
            return {
                DataShape: 'Table',
                Height: 400,
                Width: 400,
                MarkerColor: 'rgb(255,0,0)'
            };
        },
        visObjectType: symbolVis
    };


    symbolVis.prototype.init = function init(scope, elem, piwebapi) {
        piwebapi.ConfigureInstance("https://marc-web-sql.marc.net/piwebapi", true);
        
        console.log('Starting init scope: ' + scope);
        console.log('Starting init scope: ' + elem);


        this.onDataUpdate = dataUpdate;
        this.onConfigChange = configChanged;
        this.onResize = resize;
        
        function configChanged(config, oldConfig) {
            console.log('configChange called');
        };


        function resize(width, height) {
            console.log('resized called');
        }


        function dataUpdate(data) {
            piwebapi.home.get().then(function (response) {
                scope.data = response.data;
            });
        }
    }
    PV.symbolCatalog.register(definition);
})(window.PIVisualization);

 

Create a new file name sym-piwebapi-basic-sample-template.html on the %PIHOME64%\PIVision\Scripts\app\editor\symbols\ext folder with the following content:

 

<center>
    <br /><br />
    <p style="color: white;">{{data}}</p>
</center>

 

You can find more custom PI Vision symbols samples that inject piwebapi service here.

 

Time to test our custom library. Dragging any element and dropping it on the screen, we can see that JSON response from the main PI Web API endpoint.

 

 


Conclusions

 

I hope that using the PI Web API client library without changing the PI Vision 3 source code will make our customers and partners use this library even more. On my next update, the library will be able to build WebIDs client side.

Overview

We often come across scenarios in which we would like to extract large data sets into files (.txt, .csv etc) from the PI Data Archive but run into parameters which are put in place to protect the PI Server from trying to satisfy a huge query. The server can handle larger and larger queries with more resources but these safeguards prevent queries from straining the archive leading to performance issues. There are valid cases where these safeguards become limitations making it difficult in achieving our intended objectives.

 

There are three PI tuning parameters that can help (For a more detailed discussion refer to KB 3224OSI8)

(1) ArcMaxCollect to limit the quantity of data retrieved by a single client

(2) MaxThreadsPerClientQuery to restrict the number of repeated queries accepted from a single client

(3) Archive_MaxQueryExecutionSec to cancel long-running archive queries.

 

Working around the ArcMaxCollect contraint

The most common scenario of running into a limitation is with ArcMaxCollect which causes PI Clients to return "Failed to retrieve events from server. [-11091] Event collection exceeded the maximum allowed" after large or filtered queries. The problem is that you are asking the PI Data Archive for too large an amount of information at once.

 

In order to get around we need to break down the underlying query into smaller queries. To preserve data integrity we need to make sure no values are excluded and more values are not included from the boundaries while collating the chunks. Rick Davin has addressed this issue in his blog post GetLargeRecordedValues - working around ArcMaxCollect and the users interested in the code implementation are highly encouraged to check it out. This is incorporated into the PIEventsNovo utility's downloadcsv option to get the values from PI Points into csv files.

 

Usage of the Utility

pieventsnovo.exe -downloadcsv <tagmasks> <starttime,endtime>[,pageSize] [-server Archive]

 

Example:

 

The pageSize here refers to the "chunking" of the data retrieval from the server. The Defaults, Min and Max can be set in App.config. When it is not specified in the command line the Default is used.

The output folder is also specified in the App.config. If it is left as an empty string the the applications current directory is used.

 

For technical documentation, other available features & to download use the following PI DataPipe Events Subscription and Data Access Utility using AF SDK - PIEventsNovo

 

A note of caution

Retrieving larger number of values may potentially result in a higher load/resource usage on the server. Make sure adequate resources are available in order to protect the server and the network (and indirectly, client applications) from enormous amounts of data being retrieved.

Opening .csv files using MS Excel may run into specifications and limits set by Microsoft. For 2016 version this is 1,048,576 rows. Alternatively use editors like NotePad, Emacs, gedit, Notepad++ etc.

Filter Blog

By date: By tag: