First released in version: 3.6 / Last updated in version: 3.9

External Definition API

The following describes using external definition rules to create a .NET plugin with callbacks, performing actions on property changes.

 

Installing .NET API

Requirements

In order to use the .NET API, you require SmartSpace platform version 3.6 or higher with a license for the Rules engine developer.

You also need:

  • .NET Core SDK 6.0.x

  • UbisensePlatform and UbisenseApiBase NuGet packages

  • ubisense_external_code_generator command-line tool

Installation

Install Rules engine developer

If you do not already have the Rules engine developer, make sure the SmartSpace platform includes a license for it, and install the feature using Service Manager. (For further information on all aspects of installation, see SmartSpace Installation.)

Install ubisense_external_code_generator

You can install ubisense_external_code_generator from either one of two sources:

Developer Tools for .NET Core

If you have not already installed it, you need the .NET SDK.

UbisensePlatform and UbisenseApiBase NuGet packages

You can download the UbisensePlatform and UbisenseApiBase NuGet packages from the Ubisense NuGet server at https://nuget.ubisense.net/.

You can also find the UbisensePlatform and UbisenseApiBase NuGet packages in the \api\dotnet subdirectory of your distribution directorys.

Setting up the package source

Download the Ubisense package files to an accessible location, for example C:\Ubisense\packages. Now use one of the following methods to add the package(s) to your project.

Demo

We will use a worked example in this document. The example dataset has the following objects/properties already added to the UDM:

  • A DampedObject type with the following properties:

    • An Alpha property of type Double;

    • A Damped property of type Double;

    • A Reading property of type Double;

  • A DampedObject named Object1 with an Alpha value between 0 and 1.

In the example, we will create a service that applies a damping filter whenever a reading is updated, with the following logic:

Whenever we have a new reading for an object X, set
damped(X) = alpha(X) * reading(X) + (1 – alpha(X)) * damped(X)

Configuring the external relationship

We configure the external relationship in BUSINESS RULES in SmartSpace Config:

  1. Create a new external definition by double-clicking <Create new definition> and choosing external definition.

    The name of this definition will determine the name of the DLL we will generate later.

    Our demo uses the name DemoExternal.

  2. Add the required properties to the definition. Our example service we will need to read the Alpha, Damped and Reading properties as well as set the Damped property. We will need to drag all three properties to the slot under external definition uses and the Damped property to the slot under and sets.

  3. Publish the external definition.

Creating a plugin

Plugin creation requires the ubisense_external_code_generator tool. See Install ubisense_external_code_generator for more information.

Code Generator

The code generator tool generates C# code for accessing the SmartSpace properties defined in your external definition, with events for property value changes. Generated code implements interfaces defined in the UbisenseApiBase package, these interfaces are outlined later in this document. Generated code will be output to the “output/” directory and will contain the following classes:

  • UDM UObject types used by the properties.

  • Accessors for the properties with event handlers, implementing IGetter, ISetter and ICallback.

  • KeyRows for the properties, implementing IKeyRow.

  • A wrapper class, containing all accessors and an establish event handler. The class will be named <External name>Wrapper where external name is in Pascal case.

We will run the tool with the name of the external definition we published, DemoExternal. The result should look like the following:

Implementing the plugin

To create a plugin, create a .NET standard class library and add the generated code files to it.

Adding package dependencies

The generated code depends on two Ubisense NuGet packages, UbisensePlatform and UbisenseApiBase. If your project uses package references, you can include these dependencies by adding the following to your .csproj file.

<ItemGroup>
    <PackageReference Include="UbisensePlatform" Version="3.*" />
    <PackageReference Include="UbisenseApiBase" Version="3.*" />
</ItemGroup>

Implementing the callback class

Define a class that derives from the wrapper class. This will be the entry point to your plugin. Add callback functions to the relevant events within the class’ constructor.

We implement our callback to set the damped value in example_client.csproj in the file DemoExternalImplementation.cs file. The files in the Autogenerated/ directory are those generated by the code generator tool. The implementation is as follows:

namespace Ubisense.UDMAPI
{
    public class DemoExternal : DemoexternalWrapper
    {
        public DemoExternal ()
        {
            Log.Enabled = true;
            Reading.update += Reading_Update;
            Reading.insert += Reading_Insert;
        }

        private void Reading_Insert(ReadingKeyRow key, double newValue)
        {
            if (!Alpha.GetValue(key.DampedObject, out double alpha))
            {
                return;
            }
            if (!Damped.GetValue(key.DampedObject, out double damped))
            {
                damped = 0.0;
            }
            double v = alpha * newValue + (1 - alpha) * damped;
            Log.WriteLine($"Updating damped to {v}");
            Damped.SetValue(key.DampedObject, v);
        }

        private void Reading_Update(ReadingKeyRow oldKey,
                                    double oldValue,
                                    double newValue)
        {
            Reading_Insert(oldKey, newValue);
        }
    }
}

Here we derive from the wrapper class and implement the logic to set the damped value in the Reading_Insert method, adding callbacks to the reading insert and update events in the constructor. The names of key properties in the KeyRow types generated are based on the type of that key, for example “key.Damped_Object”. If a property used has more than one key column with the same type, then it adds numbers, so you might have key.Damped_Object1 and key.Damped_Object2.

Note: There is a “.Log” property in the wrapper which, if enabled, can be used to write to the “plugin” platform monitor stream.

Loading the plugin

Plugins are hosted by the Ubisense:Business rules:External plugin host service. Upon loading a plugin, the service will create an instance of your wrapper class and run until terminated.

Loading a plugin

Define the plugin directory. This is a local parameter external_plugin_directory and will need to be set in so in the registry or platform.conf on the machine running the service.

If a plugin directory is not configured, the host exits and a trace message is generated:

typed_api_host: plugin directory not set

If the configured plugin directory does not exist the following message is generated:

typed_api_host: plugin directory does not exist

Publish your classlib and copy the directory into the external_plugin_directory. The classlib directory must have the same name as the classlib’s main DLL, excluding file extension. The host service will automatically load all plugins in the external_plugin_directory on startup as well as any new plugins added to the directory during runtime.

For our example we will set external_plugin_directory to C:\UbisensePlugins (this may need to be set to a different value on your machine). Ensure the external plugin directory exists (and is empty). Publishing the example_client project will generate an assembly named DemoExternal.dll along with all its dependencies. The assembly name DemoExternal is set in example_client.csproj. This assembly name is the same as our class implementing the DemoExternalWrapper class above.

For example the easiest way to build our DemoExternal assembly is to run:

dotnet publish -o DemoExternal

This will create a DemoExternal folder under the current directory and this entire folder, containing the host service and all its dependencies, can be moved into the plugin folder we defined to install it. In our example this means moving DemoExternal and its contents into C:\UbisensePlugins.

Its plugin service will load this plugin immediately (or the next time it is started if the service is not currently running) and create an instance of the DemoExternal class, running indefinitely and listening for callbacks.

Unloading a plugin

Plugin files will be locked once loaded. To unload a loaded plugin the service must first be stopped, then the directory removed.

Caching of properties

When an external definition is not acting purely on property changes, but instead has code in the "Run" method, then queries on these properties may return no results in error as not all the properties will have been loaded yet. The method Filtering.WaitForPropertyCache waits for properties to be loaded when the Run method is called before running queries on the properties. This is implemented by overriding the Run method in your implementation. For example:

namespace Ubisense.UDMAPI
{
    public class Exporter: ExporterWrapper
    {
       …
       override public void Run()
       {
		Log.WriteLine($"before waiting for property cache Damped contains {Damped.GetAllRows().Count} rows");
		Filtering.WaitForPropertyCache(IsCellular()) ;
		Log.WriteLine($"after waiting for property cache Damped contains {Damped.GetAllRows().Count} rows");
       }
       …
   }
}

In this example, ensure that Damped contains a large number of rows. You should be able to see in the log messages that before Filtering.WaitForPropertyCache has been called, Damped contains 0 rows. After Filtering.WaitForPropertyCache has been called, all the properties will have been loaded and the log message will show the correct number of rows.

Threading of callbacks

Plugin callbacks are added to an action queue, with a single thread raising callback events per plugin. The process, from property value changed to plugin callback method execution is as follows:

  1. A change is made to a UDM property used by a loaded plugin.

  2. The main UDM callback manager, shared between all plugins, sees the change and passes the event to each plugin’s callback manager.

  3. A plugin callback manager sees the change and adds it to queue of UDM change events to be raised. Each plugin has a single plugin callback manager for all properties.

  4. A Timer regullarly raises an event on the ThreadPool at regular intervals to trigger all pending UDM change events. With a plugin, UDM change events are guaranteed to be raised in the order they were received and the next UDM change event will not be raised until the previous UDM change event callback has completed. There will be at most one thread per plugin raising UDM change events at any time, so methods attached to these events should terminate in reasonable time.

  5. A UDM change event is raised within an accessor and passed to the attached method(s) in the Wrapper implementation.

In our example, changing a reading value would look like the following:

  1. The host service starts, loading our plugin and creating an instance of our DemoExternal class.

  2. The DemoExternal’s accessors register their properties with the main UDM callback manager.

  3. A Reading value is changed for a DampedObject, e.g. we set a new reading in SmartSpace Config.

  4. The main UDM callback manager sees the change and passes it to the callback manager used by our plugin’s classes

  5. Our plugin’s callback manager adds an update event to the UDM change events action queue.

  6. The timer triggers and an available thread begins executing actions in our UDM change events action queue, raising an update event for the DampedObject:Reading property.

  7. The update event is raised and the DemoExternal::Reading_Update method is executed.

Threading for non-event-based processes

There is also support for a plugin to have its own thread procedure so it can do non-event-based things (such as periodically checking/processing something). This is implemented by overriding the “Run” method in your implementation. The host service will call the Run method in a new thread once the plugin has been loaded and the constructor called. For example:

namespace Ubisense.UDMAPI
{
    public class Exporter: ExporterWrapper
    {
       …
       override public void Run()
       {
          while (true) 
          { 
               DoSomethingPeriodically();
               System.Threading.Thread.Sleep(1000);
           } 
       }
       …
   }
}

Monitor Streams

typed_api_host

The External plugin host service prints some useful status messages on the typed_api_host monitor stream.  This can be used to see whether the plugin is loaded correctly.

To enable the trace stream, use the command:

ubisense_configuration_client set platform_monitor typed_api_host

and then restart the External plugin host service.

Example messages are:

typed_api_host: initialising plugin C:\Ubisense\plugins\DemoExternal
typed_api_host: creating instance of callback class type DemoExternal

typed_api_callbacks

The invocation of callbacks in the Typed API can be traced using the typed_api_callbacks monitor stream.

Use the command:

ubisense_configuration_client set platform_monitor typed_api_callbacks

and then restart the relevant services (the site- and cell-level typed API hosts). When properties change you should see messages of form similar to some of these:

native callback manager: establish
native callback manager: schema changed
native callback manager: data inserted for <property>
native callback manager: data updated for <property>
native callback manager: data removed for <property>
Callback Manager OnInsert(<property>,..) callback count = <number of invocations>
Callback Manager OnUpdate(<property>,..) callback count = <number of invocations>
Callback Manager OnRemove(<property>,..) callback count = <number of invocations>
Callback Manager OnEstablish
exception in callback: <message>
flushed callback queue, total completed = <completed>, total exceptions = <exceptions>

UbisenseApiBase Interfaces

The generated files will all be in the Ubisense.UDMAPI namespace. The interfaces uses are outlined below.

IKeyRow

Represents a single row of property keys (without value).

Properties

Keys Get the collection of keys

Methods

ToKeyList() Get the collection of keys in string form

IPropertyGetter

Get property rows/values

Methods

GetValue(TKeyRow, Object) Get the value of a row with the provided key
GetAllRows() Get all row key/values pairs

IPropertySetter

Set property values

Methods

SetValue(TKeyRow, Object) Set the value of a row with the the provided key to the provided value, overriding if already set
DeleteRow(TKeyRow) Delete a row with the provided key

IPropertyCallback

Provides property row events

Events

insert Raised on creation of a new row
update Raised on a row value being changed
delete Raised when a row is deleted