Stateful Programming

Starting and Stopping

To make SCI be able to do its work, its Start() method must only be called once at the very beginning of the hosting application’s lifetime. This method will let the SCI startup its application wide background workers dealing with internal threading, queues and caches. These background workers run and exist until the hosting application ceases and goes out of memory. To properly end the hosting application, it is mandatory to call the Stop() method of SCI. This makes sure that the background workers are nicely shutdown and disposed of. If this call to SCI is omitted, the hosting application might reside in memory, after its closure!

TCP Initialization

The static Initialize() method is used to pass general communication parameters to the background workers dealing with the TCP communication to the inmation Core. Note that calling this method does not actually start the communication, but only sets internal parameters. The communication will only be established when the first function is called that needs to connect to the Core. To use different communication parameters, the currently used parameters needs to be invalidated by calling the static Reset() method. This will also disconnect SCI from the Core. Do not call Initialize() a second time, without a Reset() before!

Simple Synchronous Read & Write

The most simple calls are synchronous reads and writes. Within a couple of lines of code it is possible to read from or write to a data source connected to the system. Be aware that synchronous calls block the hosting application until they return and are always initiated from the client side. In order to get updates of a value on its change, use the subscription mechanism supplied by the SCI.

Example: Simple SCI console application executing a synchronous read on an OPC DA Item

using System;
using inmation.api;

namespace SimplestSCI
{
    class Program
    {
        static void Main()
        {
            SimpleCallInterface.Start();
            SimpleCallInterface.Initialize(
                new TcpConfig() {
                    HostNameOrIp = "CoreHost",
                    Port = 6512 }
                , true);
            SimpleCallInterface sci = new SimpleCallInterface();

            ReadItem readItem =
                new ReadItem("/System/Core/Connector/OPC DA DataSource/Item1");
            Result result = sci.ReadValue(new SecurityCredentials() { ProfileName = "so", Password = "password" }, readItem);

            Console.WriteLine(readItem.Timestamp.ToString() +
                              " : " +
                              readItem.Value.ToString());

            SimpleCallInterface.Reset();
            SimpleCallInterface.Stop();

            Console.WriteLine("Press ENTER to quit");;
            Console.ReadLine();
        }
    }
}

This is the bare minimum of code you need to read the value of an OPC DA item from a remote OPC Server connected to the system!

Of course, in a production environment error handling would have to be added.

The next example shows how to handle errors. Try this simple application in various scenarios to understand how SCI deals with errors.

  • Pass an incorrect hostname and/or port for the TCP communication parameters

  • Pass an incorrect/insufficient login information when calling read

  • Create a read item of non-existing path

In each case check what code line will show the error.

Example: Simple SCI console application executing a synchronous read with error handling

using System;
using inmation.api;

namespace SimplestSCI_ErrorHandling
{
   class Program
   {
       static void Main()
       {
           //call only once per application lifetime
           SimpleCallInterface.Start();
           //initialize tcp parameters of Core communication
           SimpleCallInterface.Initialize(
               new TcpConfig()
               {
                   HostNameOrIp = "192.168.0.104",
                   Port = 6512
               }
               , true);
           //get sci instance for non-static functions
           SimpleCallInterface sci = new SimpleCallInterface();

           //read the value of a path, here an OPC Item
           //note that no property is indicated in the path,
           //which implicitly requests the ItemValue property
           ReadItem readItem =
               new ReadItem("/System/Core/Connector/OPC DA DataSource/Item1");
           //pass credentials of the system owner and read
           Result result = sci.ReadValue(new SecurityCredentials() { ProfileName = "so", Password = "password" }, readItem);
           //if the communication worked in principle
           if (result.Succeeded())
           {
               //if reading the requested path worked
               if (readItem.Result.Succeeded())
               {
                   //show received data
                   Console.WriteLine(readItem.Timestamp.ToString() +
                        " : " +
                        readItem.Value.ToString());
               }
               else
               {
                   //show item level error
                   Console.WriteLine(readItem.Result.Code.ToString()
                                     + " : " +
                                     readItem.Result.Message.GetText());
               }
           }
           else
           {
               //show communication level error
               Console.WriteLine(result.Code.ToString()
                                 + " : " +
                                 result.Message.GetText());
           }

           //disconnect the tcp communication
           SimpleCallInterface.Reset();
           //stop SCI's background tasks
           SimpleCallInterface.Stop();

           //... say and good bye
           Console.WriteLine("Press ENTER to quit");
           Console.ReadLine();
       }
   }
}

In the following examples, error handling is omitted to keep the code easily readable.

Writing to a remote OPC server’s item is as simple.

Example: SCiWrite console application executing a synchronous write and read on an OPC DA Item

using System;
using inmation.api;

namespace SCIWrite {
    class Program
    {
        static void Main()
        {
            SimpleCallInterface.Start();
            SimpleCallInterface.Initialize(
                new TcpConfig()
                {
                    HostNameOrIp = "CoreHost",
                    Port = 6512
                }
                , true);
            SimpleCallInterface sci = new SimpleCallInterface();

            ReadItem readItem =
                new ReadItem("/System/Core/Connector/OPC DA DataSource/Static1");

            Result result = sci.ReadValue(new SecurityCredentials() { ProfileName = "so", Password = "password" }, readItem);
            Console.WriteLine(readItem.Timestamp.ToString() + " : " + readItem.Value.ToString());
            WriteItem writeItem = new WriteItem(22, "/System/Core/Connector/OPC DA DataSource/Static1");
            result = sci.WriteValue(new SecurityCredentials() { ProfileName = "so", Password = "password" }, writeItem);
            result = sci.ReadValue(new SecurityCredentials() { ProfileName = "so", Password = "password" }, readItem);

            Console.WriteLine(readItem.Timestamp.ToString() + " : " + readItem.Value.ToString());
            SimpleCallInterface.Reset();
            SimpleCallInterface.Stop();
            Console.WriteLine("Press ENTER to quit");
            Console.ReadLine();
       }
   }
}

Note how this example reads and writes multiple times, without calling Start/Stop and even without calling Initialize/Reset again. Doing so for each operation would be very slow. In a production environment this is typically done only once at the beginning and the end of the lifetime of your application (see also 3.1 Starting and Stopping on page 8). For this example to work in your environment, make sure that:

  • the source OPC server supports writing the selected item

  • the data type of the source item fits the one of the value being written

Should the write still fail, try adding error handling to the code. It is also recommended to familiarize yourself with the additional parameters of the WriteValue function. These allow to modify the way how the write is executed on the OPC protocol layer, which may be required with older OPC servers or ones that are under a constant heavy load.

Read History

To read historical data for a data item, SCI provides the two methods: ReadHistoricalData and ReadHistoricalDataAtTime. The concepts of using both are quite similar. The Core will store raw data for the items, but the client needs to supply the number of intervals and the aggregate to read it. A client could for example read the aggregate “Interpolative”, with 10 intervals. If 1 minute of data is requested, he will receive 6 samples of data. There is also the aggregate “Raw Data”, when using this aggregate the number of intervals is ignored. The raw data aggregate should be used with caution, since it can easily happen that large amounts of data are returned. The client can also bundle multiple items and aggregates in one history read call.

Example: Read History - Reading a series of historical data for an item

using System;
using inmation.api;

namespace SimplestSCI {

   class Program
   {
       static void Main()
       {
           SimpleCallInterface.Start();
           SimpleCallInterface.Initialize(
               new TcpConfig()
               {
                   HostNameOrIp = "CoreHost",
                   Port = 6512
               }
               , true);
           SimpleCallInterface sci = new SimpleCallInterface();

           string itemPath = "/System/Core/Connector/OPC DA DataSource/Item1";
           Aggregates aggregate1 = Aggregates.AGG_TYPE_INTERPOLATIVE;
           Aggregates aggregate2 = Aggregates.AGG_TYPE_RAW;

           HistoricalDataItem historyItem = new HistoricalDataItem(itemPath);
           historyItem.Aggregates = new Aggregates[] { aggregate1, aggregate2 };

           HistoryResponse historyResponse;
           Result result = sci.ReadHistoricalData(
               out historyResponse,                           // Container for result
               new SecurityCredentials() { ProfileName = "so", Password = "<password>" },
               DateTime.Now.AddMinutes(-1),                   // Start
               DateTime.Now,                                  // End
               false,                                         // ForSteppedLine
               10,                                            // Intervals
               new HistoricalDataItem[]{ historyItem },       // Items to read
               true,                                          // TreatUncertainAsBad
               100,                                           // PercentageBad
               100,                                           // PercentageGood
               false,                                          // UseSlopedExtrapolation
               false);                                        // rawBoundvalue

           Console.WriteLine("Samples for interpolative read");
           foreach (var intervalData in historyResponse.GetData(itemPath, aggregate1))
           {
               foreach (var sample in intervalData.List)
               {
                   Console.WriteLine(sample.T.ToString() +
                                     " : " +
                                     sample.V.ToString());
               }
           }
           Console.WriteLine("");
           Console.WriteLine("Samples for raw read");
           foreach (var intervalData in historyResponse.GetData(itemPath, aggregate2))
           {
               foreach (var sample in intervalData.List)
               {
                   Console.WriteLine(sample.T.ToString() +
                                     " : " +
                                     sample.V.ToString());
               }
           }

           SimpleCallInterface.Reset();
           SimpleCallInterface.Stop();

           Console.WriteLine("Press ENTER to quit");
           Console.ReadLine();
       }
   }
}

Event Notifications

The SCI provides a number of events that are raised on the server side and communicated to every client connected to the Core. Clients interested in such events can then react accordingly, e.g. update their display or the like.

In this document the focus is on the event raised when objects change their data, but the principle is the same for the other events too.

  • Step One: Hooking up a handler function for the requested event type

  • Step Two: Subscribe a path for the event type

Example: SCIEvents – receiving a data changed event for a remote OPC DA item

using System;
using System.Collections.Generic;
using inmation.api;

namespace SCIEvents {

   class Program
   {
        static void Main()
        {
            SimpleCallInterface.Start();
            SimpleCallInterface.Initialize(
                new TcpConfig()
                {   HostNameOrIp = "CoreHost",
                    Port = 6512  }
                , true);
            SimpleCallInterface sci = new SimpleCallInterface();

            //step one: hook up handler to event
            SimpleCallInterface.OnDataChanged += SCIOnDataChanged;

            //step two: create subscription for readItem, here for data changes
            ReadItem readItem =
                new ReadItem("/System/Core/Connector/OPC DA DataSource/Item1");
            List<ReadItem> readItems = new List<ReadItem>() { readItem };
            sci.Subscribe(new SecurityCredentials() { ProfileName = "so", Password = "<password>" }, readItems, SubscriptionType.DataChanged);

            Console.WriteLine("Press ENTER to quit");
            Console.ReadLine();
            SimpleCallInterface.Reset();
            SimpleCallInterface.Stop();
        }

        private static void SCIOnDataChanged(IEnumerable<ReadItem> items)
        {
            foreach (var item in items)
            {
                Console.WriteLine(
                    item.Path.ToString()      + " @ " +
                    item.Timestamp.ToString() + " : " +
                    item.Value.ToString());
            }
        }
   }
}

Note how as step one the OnDataChanged event is hooked up to the handler function SCIOnData-Changed, and in step two the ReadItem is subscribed. The following events are available:

Available Events
Figure 1. Available Events

Check the intellisense help for information about what each handler can be used for. Each path can be monitored for different events and in order to receive events for multiple event types, multiple subscriptions need to be created. The following subscription types are available, matching the event types generated.

Available Subscription Types
Figure 2. Available Subscription Types

Example: Code snippet showing how to use multiple event subscriptions for one path.

//step one: hook up handlers to events
SimpleCallInterface.OnDataChanged += SCIOnDataChanged;
SimpleCallInterface.OnConfigurationVersionChanged += SCIOnConfigurationVersionChanged;

//step two: create subscription for readItem, here for data changes
ReadItem readItem =
    new ReadItem("/System/Core/Connector/OPC DA DataSource/Item1");

List<ReadItem> readItems = new List<ReadItem>() { readItem };

Result result = sci.Subscribe(new SecurityCredentials() { ProfileName = "so", Password = "<password>" }, readItems, SubscriptionType.DataChanged);
if (result.Succeeded())
    sci.Subscribe(new SecurityCredentials() { ProfileName = "so", Password = "<password>" }, readItems, SubscriptionType.ConfigurationVersionChanged);

SCI Model Tree Operations

system:inmation hosts multiple models, each typically consisting of different model object types. The most often used is the I/O Model, which represents the physical infrastructure of the system, including information from connected data sources such as a remote OPC Server’s address space. SCI supplies functionality to browse the model trees, as well as to create/change/delete objects therein. Be aware that changing models’ content (esp. the I/O model) can have a severe impact on the overall system stability!

Browsing a Model Tree

To browse a model tree, call the GetChildren function for each particular parent.

Example: SCIModelOperations – GetChildren of path parent

using System;
using System.Collections.Generic;
using inmation.api;

namespace SCIModelOperations
{
    class Program
    {
        static void Main()
        {
            SimpleCallInterface.Start();
            SimpleCallInterface.Initialize(
                new TcpConfig()
                {
                    HostNameOrIp = "CoreHost",
                    Port = 6512
                }
                , true);
            SimpleCallInterface sci = new SimpleCallInterface();
            string parent = "/System/Core/Connector/OPC DA DataSource";

            List<ObjectItem> oItems;
            Result result = sci.GetChildren(new SecurityCredentials() { ProfileName = "so", Password = "<password>" }, parent, out oItems, 5000);
            if (result.Succeeded() && oItems != null)
            {
                foreach (var oItem in oItems)
                {
                    Console.WriteLine(oItem.TagName);
                }
            }

            SimpleCallInterface.Reset();
            SimpleCallInterface.Stop();

            Console.WriteLine("Press ENTER to quit");
            Console.ReadLine();
        }
    }
}

Note how the GetChildren function returns the found child elements in the out parameter, a list of ObjectItem instances. An ObjectItem is a container class, just like the ReadItem or WriteItem, used in the context of model object operations and for receiving the requested information to hold it for further processing. In the above example the TagName property of this object class is written to the console.

Other available functions to work with model tree and ObjectItem instances are:

GetParent

Gets the parent model element as ObjectItem of a specified child path.

GetChild

Gets the specified child as ObjectItem of a specified parent path.

GetObjectItem

Get an ObjectItem from a specified path, optionally including getting all model object properties and values.

Remember the possibility to subscribe to events for receiving notification when model elements change, here especially the ChildrenCountChanged and ConfigurationVersionChanged events.

Creating a New Object

To create a new object, it is required to have a decent understanding of what object types are available in the system, and what the structure of each object type looks like.

The principle steps of creating a new object are:

  • Instantiate an ObjectItem container

  • Set Model, ClassType, ObjectType property

  • Create PropertyItem container, set their values, and assign to Properties property

  • Call CreateObject

Example

SCIModelOperations – Create a new DataHolder ite

using System;
using System.Collections.Generic;

using inmation.api;

namespace SCIModelOperations
{
    class Program
    {
        static void Main()
        {
            SimpleCallInterface.Start();
            SimpleCallInterface.Initialize(
                new TcpConfig()
                {
                    HostNameOrIp = "CoreVM",
                    Port = 6512
                }
                , true);
            SimpleCallInterface sci = new SimpleCallInterface();
            string parent = "/System/CORE/Connector/OPC DA Datasource";

            //read children count before creation
            List<ObjectItem> children;
            Result result = sci.GetChildren(new SecurityCredentials() { ProfileName = "so", Password = "<password>" }, parent, out children, 1000);
            Console.WriteLine(children.Count.ToString() + " children found beneath " + parent);

            //Set basic object properties to be created
            ObjectItem oitem = new ObjectItem("");
            oitem.Model = SciModelScope.MODEL_SCOPE_IO;
            oitem.ClassType = SciModelClasses.MODEL_CLASS_HOLDERITEM;
            oitem.ObjectType = SysObjectType.OT_OBJECT;
            //prepare list of properties to be set
            List<PropertyItem> pLst = new List<PropertyItem>();
            var p1 = new PropertyItem();
            p1.RelativePath = "ObjectName";
            p1.Value = "Newly Created DataHolder";
            pLst.Add(p1);

            var p2 = new PropertyItem();
            p2.RelativePath = "Limits.OpcRangeLow";
            p2.Value = -100;
            pLst.Add(p2);

            var p3 = new PropertyItem();
            p3.RelativePath = "Limits.OpcRangeHigh";
            p3.Value = 100;
            pLst.Add(p3);

            var p4 = new PropertyItem();
            p4.RelativePath = "ArchiveOptions.StorageStrategy";
            p4.Value = "STORE_RAW_HISTORY";
            pLst.Add(p4);

            var p5 = new PropertyItem();
            p5.RelativePath = "ArchiveOptions.ArchiveSelector";
            p5.Value = "ARC_PRODUCTION";
            pLst.Add(p5);

            // Set array Property
            var p6 = new PropertyItem();
            p6.RelativePath = "CustomOptions.CustomProperties.CustomPropertyName";
            p6.Value = new string[] { "CustomAlias", "CustomInfo" };
            pLst.Add(p6);
            var p7 = new PropertyItem();
            p7.RelativePath = "CustomOptions.CustomProperties.CustomPropertyValue";
            p7.Value = new string[] { "DHolder1", "Holds sample data" };
            pLst.Add(p7);

            oitem.Properties = pLst;

            //call create
            List<ObjectItem> oitems = new List<ObjectItem>() { oitem };
            sci = new SimpleCallInterface();
            var rs = sci.CreateObject(new SecurityCredentials() { ProfileName = "so", Password = "<password>" },
            //read children count after creation
            result = sci.GetChildren(new SecurityCredentials() { ProfileName = "so", Password = "<password>" }, out children, 1000);
            Console.WriteLine(children.Count.ToString() + " children found beneath " + parent);

            SimpleCallInterface.Reset();
            SimpleCallInterface.Stop();
            Console.WriteLine("Press ENTER to quit");
            Console.ReadLine();
       }
   }
}

Note how the PropertyItem container objects are created and their properties are set. To understand what object type uses what properties, refer to the system documentation, or use DataStudio’s Object Properties panel for details.

Advanced users can also use SCI’s ClassDefinition class, to get information about the structure of object types.

ClassDefinition Class
Figure 3. ClassDefinition Class