Greetings, fellow geeks!  I hope to occasionally find time to write blog posts that are hopefully meaningful to others.  Like many other vCampus customers, I consider myself to be working deep in the trenches where we oftentimes must supplement the OSIsoft offerings with some of our own patchwork code to provide a more comprehensive solution, or even just a simple workaround to our own customers.  It’s in the spirit of finding such solutions that I would dedicate my own blogs.

 

 

 

My inaugural blog will cover the topic of time.  Or more so, the resolution of time as it may be on a client front-end and how that may differ from what might be coming from a backend PI server.  This topic may be particularly relevant to anyone needing to work with sub-second data.

 

 

 

A couple years back I had a gnarly 3-day bug hunt trying to find out why the AFTime value coming back from a PI Point didn’t exactly match the start time from a GetValues call.  Some info about my environment: I was testing on my 64-bit Windows 7 desktop against an AFAttribute using the PI Point data reference.  The PI Point was interpolated, i.e. not stepped.  I would set the end of the time range to AFTime.Now, and set the start to 8 hours before the end.  And for reasons that shall remain undisclosed, I was checking that the passed start time (now minus 8 hours) was exactly equal to the first returned timestamp for the PI Point over that time range.

 

 

 

I was quite surprised to find out that they weren’t exactly the same – when I thought they should be.  In fact, it took me over 2.5 days to find that much out.  The fact that they weren’t equal was slightly obscured as I initially dumped out the UtcSeconds for the timestamps.  What I first discovered was something like:

 

 

 

AFTime.Now.UtcSeconds      1334259544.15274

 

Returned.UtcSeconds        1334259544.15274

 

 

 

Hey, those look equal to me.  How about you?  But something still wasn’t right, so I had to dig deeper.  I decided to chop off the whole number portion of UtcSeconds and focus on the decimal portion.

 

 

 

AFTime.Now.UtcSeconds      1334259544.15274

 

              Whole        1334259544

 

              Decimal      0.152743101119995

 

 

 

Returned.UtcSeconds        1334259544.15274

 

              Whole        1334259544

 

              Decimal      0.152740240097046

 

 

 

Ahhhh, now my interest got peaked.  This times are indeed different staring in the 6th decimal place.  In this example, I show a difference of:

 

 

 

Delta  2.86102294921875E-06 of one second

 

 

 

I began to ponder just how could that be?  At first I wondered if that’s attributable to the difference between AFTime versus PITime.  But, no, it’s wasn’t  that.  It soon dawned on me that the problem was my development environment had a high resolution timer for when AFTime.Now is invoked.  And while a sub-second PI server can serve up sub-second data, that sub-second resolution will be in intervals of 65536 sub-second slices per second.  That was my problem: AFTime.Now had a finer sub-second resolution and wasn’t guaranteed to be on a ‘whole’ PI sub-second slice (if I have explained that adequately) and therefore differed from the returned PI server time’s UtcSeconds starting in the 6th decimal place.  I did mention gnarly, right?

 

 

 

For those of us who can live with whole second data, OSIsoft has 2 methods worth mentioning.  Ever since AF 2.3, the AFSDK offers both the AFTime.NowInWholeSeconds property, compliments of yours truly.  Not that I wrote it, but I did request it.  And there is also the AFTime.TruncateToWholeSeconds method.  These hopefully should satisfy many of you, except perhaps those in Electrical or Power industries where sub-second timings are critical.  For those, I offer a bit of code to compliment the TruncateToWholeSeconds method.

 

 

 

I call the extension method TruncateToPISubseconds where again I define a “PI server sub-second” as 1/65536th of a second.

 

 

 

C# Extension Method

 

 

 

using OSIsoft.AF.Time;

 

 

 

public static class SomeTimeExtensions

 

{

 

    public static AFTime TruncateToPISubseconds(this AFTime inputTime)

 

    {

 

        AFTime outputTime = new AFTime(inputTime);

 

        double Seconds = outputTime.UtcSeconds;

 

        double WholeSeconds = System.Math.Truncate(Seconds);

 

        if (Seconds != WholeSeconds)

 

        {

 

            const int SlicesPerSecond = 65536;

 

            double Slices = (Seconds - WholeSeconds) * SlicesPerSecond;

 

            double WholeSlices = System.Math.Truncate(Slices);

 

            if (Slices != WholeSlices)

 

            {

 

                double Subseconds = WholeSlices / SlicesPerSecond;

 

                outputTime = new AFTime(WholeSeconds + Subseconds);

 

            }

 

        }

 

        return outputTime;

 

    }

 

}

 

 

 

VB.NET Extension Method

 

 

 

Imports OSIsoft.AF.Time

 

 

 

Public Module SomeTimeExtensions

 

 

 

    <System.Runtime.CompilerServices.Extension()> _

 

    Public Function TruncateToPISubseconds(inputTime As AFTime) As AFTime

 

        Dim outputTime As New AFTime(inputTime)

 

        Dim Seconds As Double = outputTime.UtcSeconds

 

        Dim WholeSeconds As Double = System.Math.Truncate(Seconds)

 

        If Seconds <> WholeSeconds Then

 

            Const SlicesPerSecond As Integer = 65536

 

            Dim Slices As Double = (Seconds - WholeSeconds) * SlicesPerSecond

 

            Dim WholeSlices As Double = System.Math.Truncate(Slices)

 

            If Slices <> WholeSlices Then

 

                Dim Subseconds As Double = WholeSlices / SlicesPerSecond

 

                outputTime = New AFTime(WholeSeconds + Subseconds)

 

            End If

 

        End If

 

        Return outputTime

 

    End Function

 

 

 

End Module

 

 

 

In summary, there are 2 bits of advice that I give to every developer new to time series data:

 

 

 

First, is to never just use AFTime.Now as is, and especially not in a loop.  Since Now changes with every invocation, you run the risk that data returned is not at the same event horizon, even if they are mere milliseconds apart.  Rather you should freeze AFTime.Now into a variable, and use that variable’s value for the duration of your procedure.

 

 

 

And second, you should consider truncating AFTime.Now to an acceptable resolution as required by your application.  If your PI data is saved to whole seconds, then use one of the AFSDK methods to truncate Now to whole seconds.  If you are using sub-second PI, that is 1/65536th of a second data, you may find the above extension methods useful.  If you work with whole minutes, there’s nothing stopping you from writing your own extension method!