# Custom Connector Module Development

This document is meant to present an example of implementation of ABB Ability™ Edge module acting as a connector to cpmPlus RTDB server (as an example). The goal of the provided codebase (and this publication) is for the user who is looking to build their own connector module to have a good understanding of how to accomplish this. This publication uses ABB cpmPlus RTDB as an example of a target system that the connector will communicate with. However, the presented concepts can be used while developing any other connector, no matter what the actual target interface/system is.

# Before You Start

It's advised to follow the Edge Development Tutorial Series first.

# Overview

Since cpmPlus RTDB is used as a base of our example, it is a good idea to have at least a basic understanding of what it is. The code base accompanying this article will make much more sense then. Quotation from official cpmPlus Documentation states:

(...) cpmPlus History is a time series database (TSDB) that offers a relational database core (RTDB) designed and optimized for industrial process information management and extensive history recording.

In simple terms, cpmPlus RTDB is a database that enables storage of timeseries type of data in variables. It has interfaces to write and read the data. In this article we will be reading data from the system using the C# library that comes with RTDB.

The module presented in this document covers two use cases:

  • fetching last value of chosen variable
  • fetching values of chosen variable for a specified time frame

Such functionality is rather common, therefore the presented case could be easily used as a reference to connect to other systems.

# General Architecture

One example of ABB Ability Edge connector module is the official cpmPlus Connector. It takes data from RTDB and sends it to the ABB Ability™ cloud directly. Our custom module works a bit differently - it is more like a proxy:

  1. It listens for requests from other modules
  2. It handles the requests (e.g. "get me some data")
  3. It returns results for the requests to the modules that sent the requests in the first place

It is therefore possible to create additional modules that work as clients of our connector. It provides separation of concerns, i.e. your client modules can focus on their business logic without handling RTDB communication. The illustration below presents this architecture (generically):

Custom cpmPlus modulearchitecture

The illustration shows three main "objects":

  • ABB Ability™ Cloud
  • ABB Ability™ Edge
  • Target system (in our example it is cpmPlus RTDB)

ABB Ability™ Edge contains:

  • Proxy module - module running on every Edge, it communicates with the cloud and administers the Edge
  • MQTT Broker module - another module running on every Edge, it enables communication between the modules using MQTT protocol
  • Connector module - module being the topic of this publication, it will connect to cpmPlus RTDB system to fetch data from it
  • Connector module's clients - clients of connector module, they can request the data that cpmPlus module delivers from RTDB

The illustration contains arrows showing how each object communicates with the others. Solid lines represent "real" connections. Dashed lines present logical connections. Clients of the cpmPlus module do not talk directly with it - they do it through the MQTT Broker.

# Example Scenario

The scenario described in this publication will consist of:

  • One cpmPlus module
  • One cpmPlus module client

Both modules are included in a code repository (more on that in the next section). The client is a very simple implementation, which indefinitely sends requests in constant intervals.

However, nothing stops you from instantiating multiple clients or even multiple cpmPlus modules - each of them could connect to a different cpmPlus server.

# Code Repository

The code described in this publication resides in the ABB CodeBits repository. It has two branches (and possibly some other, not important for this article):

  • master - contains "basic" version of the solution - only last value fetch of variables is implemented. Client module requests the value of the same hard-coded variable in a constant time interval.
  • feature/add-timeframe-fetch-of-variable - feature branch containing an updated version of the solution, where cpmPlus module accepts both last value requests and values from specified time frame requests. Client is also updated
    • it requests both last value and time frame values of the same hard-coded variable as before.

In this article you will be walked through how to take the "basic" version (from master branch) and extend its functionality to the level provided by "feature/add-timeframe-fetch-of-variable" branch. However, nothing stops you from just using the already extended version.

TIP

Provided code is meant as an example and base for further development. Readers can use and modify it however they want.

# Docker Images

In order for the Edge to be able to instantiate the modules, they need to exist in some Docker registry. In order to upload them, a simple script - DockerRelease.sh - was created. It does the following actions:

  1. Logs into https://abbability.azurecr.io registry
  2. Builds the whole solution
  3. Builds images
  4. Pushes images to the registry

WARNING

Do not push your own versions of the modules under CST tag (starting with abb.ability.cst). Instead, create your own tags, preferably including your division's name. That way, you will not mistakenly overwrite modules. Remember that such change requires you to also update modules' type definitions since they include URLs to registry. More on type definitions in the next section.

# Type Definitions

Before any business module starts on your Edge, it needs to be defined in the cloud. The necessary type definitions are included in "TypeDefinitions" directory in the code repository.

# Connector module

First, the modules' type definitions should be POSTed, in the following order:

  1. abb.ability.configuration.edge.modules.customCpmPlusModule
  2. abb.ability.device.edge.modules.customCpmPlusModule

The module's configuration type definition includes the following information:

  • connection string
  • user name
  • password

This data can be put directly in the type definition (abb.ability.configuration.edge.modules.customCpmPlusModule) or you can apply it later by updating the Information Model object of your module.

TIP

If you intend to run your version of the module, which preferably lives under your own tag in the Docker registry, update the module's URL in the type definition. Same thing applies to cpmPlus module client, mentioned below.

# Client module

Next, cpmPlus module's client (which is another module) should be defined:

  1. abb.ability.configuration.edge.modules.customCpmPlusModuleClient
  2. abb.ability.device.edge.modules.customCpmPlusModuleClient

TIP

No additional configuration is available here, however, in practice, there could be a property like "cpmPlusModuleId" to be able to set the cpmPlus module's ID. In our simple client this ID is hard-coded.

# Edge device

Last type definitions to POST are the ones associated with Edge device itself:

  1. abb.ability.configuration.cpmPlusEdgeMachine
  2. abb.ability.device.edge.cpmPlusEdgeMachine

As you can see in the JSON of abb.ability.device.edge.cpmPlusEdgeMachine, both cpmPlus module and its client are referenced.

# Codebase

# Projects

The provided .NET Core solution contains three main projects (presented in subsections below). Other than this, testing projects are also provided (incorporating XUnit2 and AutoFixture).

# Common

The Common project contains classes used by other projects. It is a .NET Standard 2.0 library project, so it can be used by .NET Core applications.

Included services:

  • ConsoleLogger - simple logger used in the solution, in your project you could use your preferred logging library. For simplicity, ConsoleLogger is a very simple implementation that can log on three levels: Informational, Warning, Error.
  • EdgeConfigurationLoader - reads module's environment variables (MQTT topics, objectId, etc.)
  • MqttClient - wrapper around IManagedMqttClient provided by MQTTnet package.
  • ModuleBase - abstract class created as a base for any module (used by both cpmPlus module and its client). It deals with fundamental ABB Ability™ Edge module operations - setting up MQTT, handling configuration updates from the cloud, handling commands invocations from the cloud.

All classes listed above implement interfaces. Association between classes (in all projects of the solution) is done using composition. Classes depend on interfaces, not on concrete implementations. Thanks to these facts, services are testable.

# CustomCpmPlusEdgeModule

Before describing how the module works, its simplified composition UML will be presented:

CustomCpmPlusEdgeModule's compositionUML

As you can see, some of the services are inherited from ModuleBase. However, many more are added. Here's a description of each interface:

  • IRtdbConnection - service handling cpmPlus connection (it holds a reference to cDriverSkeleton instance). Connection is done asynchronously via ConnectAsync method, which allows cancellations in case of configuration update.
  • IRtdbConnectionFactory - since the module has the need to create new connection when configuration gets updated, it includes an instance of IRtdbConnectionFactory implementation, which provides new IRtdbConnection each time it's needed. Before requesting new connection object, module disposes old one - two connections at the same time are not needed.
  • IRtdbVariableReader - service handling fetching of variables from RTDB. It uses IRtdbConnection (same instance as the module itself) to access RTDB.
  • IRequestQueue - a queue for requests from other modules.
  • IRequestHandler - its goal is to read requests from IRequestQueue and handle them. Since it has IRtdbVariableReader reference, it uses it to realize the requests.
  • IModuleConfigurationParser - since the module can receive configuration updates from the cloud, a service is needed to parse the configuration into ModuleConfiguration object. This object is used to check if RTDB connection needs to be reset.

# Module's configuration and its updates

Module allows three properties to be configured:

  • RTDB connection string
  • RTDB username
  • RTDB password

These settings are stored in an object of class ModuleConfiguration:

public class ModuleConfiguration : IEquatable<ModuleConfiguration>
{
    public ModuleConfiguration(string cpmConnectionString, string cpmLogin, string cpmPassword)
    {
        CpmConnectionString = cpmConnectionString;
        CpmLogin = cpmLogin;
        CpmPassword = cpmPassword;
    }

    public string CpmConnectionString { get; set; }
    public string CpmLogin { get; set; }
    public string CpmPassword { get; set; }

    ...
}

Methods resulting from IEquatable<T> interface implementation were skipped for readability.

Whenever a new configuration comes (and it varies from the previous one):

  1. IRequestHandler is stopped.
  2. IRtdbConnection instance is disposed.
  3. IRtdbConnectionFactory is requested to provide new IRtdbConnection object.
  4. New connection is established asynchronously.
  5. IRequestHandler continues serving requests.

During this flow, module still accepts new requests and puts them in the queue. The enumerated steps are implemented by HandleConfigurationUpdate method, which is overridden from ModuleBase:

protected async override Task HandleConfigurationUpdate(string updatedConfiguration)
{
    if (UpdateConfiguration(updatedConfiguration))
    {
        StopHandlingRequests();

        var connectionStatus = ConnectionStatus.Failed;
        while (connectionStatus == ConnectionStatus.Failed)
        {
            connectionStatus = await ConnectWithRtdbAsync().ConfigureAwait(false);

            if (connectionStatus == ConnectionStatus.Canceled)
            {
                return;
            }
            else if (connectionStatus == ConnectionStatus.Failed)
            {
                _logger?.LogWarning("Connection with RTDB failed. Is the configuration correct? Retrying after a second...");
                await Task.Delay(1000).ConfigureAwait(false);
            }
        }

        if (_communicationWithRtdbEnabled)
            StartHandlingRequests();
    }
}

TIP

HandleConfigurationUpdate is invoked from an event of new MQTT message arrival (OnMqttMessageReceived) when received message carries configuration update. Look it up in the code ModuelBase class.

In the code above, first thing happening is a condition that checks if the incoming update actually brings any changes. It might happen that incoming configuration does not change cpmPlus-related settings. In such case, we do not really have anything to do - update is ignored.

Steps: 2, 3 and 4 are handled by ConnectWithRtdbAsync facade method.

Connection can have one of three statuses:

  • Connected - successful connection with RTDB established
  • Failed - connection failed, because RTDB endpoint could not be reached
  • Canceled* - connection was aborted. Such situation happens when configuration update is delivered while connection is still being established
    • cancellation token gets canceled and new connection is made

# Request-response models

Since our cpmPlus module is designed to be used in connection with other modules, some contract of communication between them needs to be established. The module is built to accept Requests and responds with Responses. These two entities were modeled in code and can be found in Common project (since they are used by both cpmPlus module and its client).

# Request

Here's a C# Request class:

[JsonObject(ItemRequired = Required.Always)]
public class Request<T> : IRequest<T> where T : RequestDataBase
{
    public Request()
    {
    }

    public Request(int requestId, string requesterModuleId, RequestDataKind dataKind, RequestOperation operation, T requestData)
    {
        RequestId = requestId;
        RequesterModuleId = requesterModuleId;
        DataKind = dataKind;
        Operation = operation;
        RequestData = requestData;
    }

    public int RequestId { get; set; } //for client to recognize response
    public string RequesterModuleId { get; set; } //requester Id - needed to send the response

    [JsonConverter(typeof(StringEnumConverter))]
    public RequestDataKind DataKind { get; set; } //Variable

    [JsonConverter(typeof(StringEnumConverter))]
    public RequestOperation Operation { get; set; } //save/get

    public T RequestData { get; set; } //startTime, endTime
}

As can be seen, the class:

  • is generic
  • implements IRequest interface

The first fact comes from the need of supporting various types of requests - different request types will bring different data with it, that's why RequestData property is of type T. The constraint for T is the necessity of it being of type RequestDataBase or its inheritance.

The second fact (interface implementation) might seem strange, since Request<T> is a DTO (Data Transport Object) class, which should not need any interface.

However, since the class is generic and we want to be able to treat it as Request<RequesDatatBase>, covariance is introduced. This applies regardless of what the actual T is, as long as it inherits from RequestDataBase. Covariance can be defined only on the level of interfaces - that's why our class implements IRequest<T>, which has the following signature:

public interface IRequest<out T> where T : RequestDataBase { ... }

Going back to the class itself, it defines the following properties:

  • RequestId - optional value to be set by requester. It will be included in a response, so that the requester can map the response to a specific request.
  • RequesterModuleId - id of a requester module - it is needed to send back a response (response topic includes recipient module's id).
  • DataKind and Operation - these two properties together specify the type of request. In basic version of the codebase, there is only one combination possible - DataKind = "Variable" and Operation = "GetLastValue". Both these properties are enums, so possible options need to be enumerated beforehand.
  • RequestData - information about the request (i.e. which variable is to be fetched? What is the time frame?).

An example of RequestDataBase inheritance is GetVariableLastValueRequestData:

[JsonObject(ItemRequired = Required.Always)]
public class GetVariableLastValueRequestData : RequestDataBase
{
    public GetVariableLastValueRequestData(string variableName)
    {
        VariableName = variableName;
    }

    public string VariableName { get; set; }
}

Request for variable last value needs just one piece of information - variable's name.

# Response

Response is modeled in a very similar fashion as a request, described previously. There is a generic Request<T> class:

public class Response<T> : IResponse<T> where T : ResponseDataBase
{
    public Response()
    {
    }

    public Response(int requestId, T responseData)
    {
        RequestId = requestId;
        ResponseData = responseData;
    }

    public int RequestId { get; set; } //for client to recognize response
    public T ResponseData { get; set; } //startTime, endTime
}

It includes:

  • RequestId - id of the request that the given response is for (same as request's RequestId)
  • ResponseData - similar to RequestData of Response<T> class, this property carries specific information about the response

Example of ResponseData is GetVariableLastValueResponseData:

public class GetVariableLastValueResponseData : ResponseDataBase
{
    public GetVariableLastValueResponseData()
    {
    }

    public GetVariableLastValueResponseData(object value, DateTime timestampUtc)
    {
        Value = value;
        TimestampUtc = timestampUtc;
    }

    public object Value { get; set; }
    public DateTime TimestampUtc { get; set; }
}

As you can see, a response for variable last value request contains: value itself and its timestamp.

# Extensibility

To support new types of requests, new classes would need to be defined:

  • new RequestDataBase inheritant
  • new ResponseDataBase inheritant

Other than that, RequestHandler would need to be updated.

# Requests queue

All incoming requests are added to the queue, which is a service called RequestQueue - it implements IRequestQueue interface. The object of this class is used by two classes:

  • CustomCpmPlusEdgeModule itself (for enqueuing new items)
  • RequestHandler (implementation of IReqeustHandler) - for serving requests from the queue

Since these two activities (enqueuing and dequeuing) happen in parallel (in separate threads), RequestQueue is a thread-safe queue implementation. It uses ConcurrentQueue internally.

Adding a new request (Enqueue method) expects a serialized request (like it is delivered via MQTT), which is later parsed into a proper object. Parsing is done using IRequestSerializer implementation, which is a simple class using Newtonsoft.Json Nuget package for doing its job.

# Handling Requests

When connection is established, by default IRequestHandler implementation is asynchronously started. It constantly watches the request queue (described in previous section) and handles each request individually. It makes use of four services:

  • IRequestQueue - queue of requests
  • IResponseSender - service responsible just for sending responses to requesting modules
  • IRtdbVariablesReader - service used to fetch variables data from RTDB;
  • ILogger - logging service

TIP

In current architecture of our module, IRequestHandler is designated to handle ALL incoming request types. However, it is a good practice to create separate services for handling various types of requests.

Module works in a simple fashion: it checks for new requests (until its job gets canceled externally - for example when Stop command is executed) and prepares responses for each of them using the mentioned helper services.

# Connection with RTDB

By default, VtrinLib library exposes synchronous interface for connecting with the RTDB. Since our use case requires ability to cancel the connection in some cases (when configuration update comes while connection is being established), a wrapper was created - RtdbVtrinLibConnection - which is an implementation of IRtdbConenction interface.

The interface, other than connecting with cpmPlus, also exposes methods to get the actual data. This was done to enable higher abstraction services (like IRtdbVariablesReader) to use IRtdbConenction as a way of fetching/saving values in RTDB. Since connection could be achieved in other ways than using official library (i.e. HTTP), the higher abstraction service still would need to function properly without knowing how the connection is actually established! That's why IRtdbConnection's methods are used by RtdbVariablesReader to get values from RTDB.

Because of that, RtdbVariablesReader's code is rather simple since it just delegates the work to the underlying connection service.

Here's how RtdbVtrinLibconnection handles last value request:

public TelemetryPoint GetCurrentValue(string tableName, string name, string additionalName = null)
{
    ThrowIfNotConnected();

    if (tableName == "Variable")
    {
        return GetVariableCurrentValue(name);
    }
    else
    {
        var variableHandle = _driver.Classes[tableName].Instances[name];
        var currentValueHandle = _driver.Classes["CurrentValue"].Instances[variableHandle];

        return new TelemetryPoint(
            currentValueHandle.GetRawPropertyValue("Value"),
            (DateTime)currentValueHandle.GetRawPropertyValue("Time"));
    }
}

The returned type is TelemetryPoint:

public class TelemetryPoint
{
    public TelemetryPoint(object value, DateTime timestampUtc)
    {
        Value = value;
        TimestampUtc = timestampUtc;
    }

    public object Value { get; set; }
    public DateTime TimestampUtc { get; set; }
}

# Remote commands

Module supports two commands to be invoked by the usage of DataAccess API:

  • start
  • stop

Their function is to start or stop IRequestHandler. Note that by default, when module is run, it is started. Command methods are decorated with InvokableMethod attribute. ModuleBase contains the logic for finding these methods (by the usage of reflection).

# ExampleClientModule

Solution contains exemplary client of our cpmPlus module. It is a very simple implementation, which uses ModuleBase for setting up all necessary module configuration (MQTT, Edge environmental configuration). Other than that, it just uses .NET Timer to send requests in a constant interval. Requests are modeled according to contact described in one of previous sections of this document.

Here's the method that is invoked for every request:

private async void SendRequestAsync(object source, ElapsedEventArgs e)
{
    _logger?.LogInfo("Sending a new request...");

    var request = new Request<RequestDataBase>(
        _random.Next(1, 1000),
        THIS_MODULE_ID,
        RequestDataKind.Variable,
        RequestOperation.GetLastValue,
        new GetVariableLastValueRequestData(REQUESTED_VARIABLE));

    await _mqttClient.PublishAsync(
        $"{_edgeEnvConfig.LocalOutTopic}/{CPM_MODULE_ID}",
        JsonConvert.SerializeObject(request));
}

Communication between modules should be a familiar topic for you, if not, please review the basic Edge modules development tutorials.

# Extending Module's Functionality

In this section, we will extend basic module functionality (fetching last value of variables) by adding another supported request type: fetching variable data from specified time frame.

TIP

The completed solution (with extended functionality included) is available on a branch feature/add-timeframe-fetch-of-variable.

# New Request Type

First thing to do is to add a new ReqeustDataBase inheritant - GetVariableHistoryValuesRequestData:

[JsonObject(ItemRequired = Required.Always)]
public class GetVariableHistoryValuesRequestData : RequestDataBase
{
    public GetVariableHistoryValuesRequestData(string variableName, DateTime startTimeUtc, DateTime endTimeUtc)
    {
        VariableName = variableName;
        StartTimeUtc = startTimeUtc;
        EndTimeUtc = endTimeUtc;
    }

    public string VariableName { get; set; }
    public DateTime StartTimeUtc { get; set; }
    public DateTime EndTimeUtc { get; set; }
}

In a case of fetching just the last value, variable's name was sufficient to handle the request. With time frame involved, startTime and endTime are required.

# New Response Type

Having a new request, we also need to have a new ResponseDataBase implementation to be able to send back the requested information - GetVariableHistoryValuesResponseData:

public class GetVariableHistoryValuesResponseData : ResponseDataBase, IEquatable<GetVariableHistoryValuesResponseData>
{
    public GetVariableHistoryValuesResponseData()
    {
    }

    public GetVariableHistoryValuesResponseData(ICollection<TelemetryPoint> data)
    {
        Data = data;
    }

    public ICollection<TelemetryPoint> Data { get; set; }
}

This class contains just one property - a collection of TelemetryPoint (the class, which was presented in one of previous sections), which is basically a collection of value-timestamp pairs.

# IRtdbConnection Update

Since our design relies on the IRtdbConnection to be the one that handles the requests on the bottom layer (via VtrinLib itself), it needs to be updated with additional method - GetValuesAsync:

Task<ICollection<TelemetryPoint>> GetHistoryValuesAsync(string tableName, IDictionary<string, string> parameters, DateTime startTime, DateTime endTime, string filter, CancellationToken cancellationToken);

Implementation should be placed in RtdbVtrinLibConnection:

public async Task<ICollection<TelemetryPoint>> GetHistoryValuesAsync(string tableName, IDictionary<string, string> parameters, DateTime startTime, DateTime endTime, string filter, CancellationToken cancellationToken)
{
    ThrowIfNotConnected();

    var query = string.Join("=? AND ", parameters.Keys) + "=?";

    var gfp = cGraphFetchParameters.CreateRawFetch(
        _driver.Classes[tableName],
        filter,
        startTime,
        endTime,
        int.MaxValue - 1,
        query,
        parameters.Values.ToArray()
    );

    var fetchResult = await _driver.FetchGraphDataAsync(new cGraphFetchParameters[] { gfp }, cancellationToken)
        .ConfigureAwait(false);

    if (fetchResult == null || fetchResult.Count() != 1 || fetchResult[0].Exception != null)
        throw new Exception($"Error while fetching values. Exception: {fetchResult[0].Exception.Message}");

    List<TelemetryPoint> result = new List<TelemetryPoint>();
    foreach (cValueRecord point in fetchResult[0])
    {
        result.Add(new TelemetryPoint(point.Value, point.TimeUTC));
    }

    return result;
}

As you can see, VtrinLib's FetchGraphDataAsync method is used, which is a recommended approach for fetching history values of variables or equipment instances in cpmPlus RTDB.

# IRtdbVariablesReader Update

Since IRtdbVariablesReader is a layer that RequestHandler interacts with to fetch data, it also needs to be updated by addition of GetValuesAsync method. Here's RtdbVariablesReader's implementation of it:

public async Task<ICollection<TelemetryPoint>> GetValuesAsync(string variableName, DateTime startTimeUtc, DateTime endTimeUtc, CancellationToken cancellationToken)
{
    return await _rtdbConnection.GetHistoryValuesAsync(
        "ProcessHistory",
        new Dictionary<string, string>
        {
            {"Variable", variableName }
        },
        startTimeUtc,
        endTimeUtc,
        null,
        cancellationToken)
        .ConfigureAwait(false);
}

Since this method just uses IRtdbConnection for the fetch, there is not much happening here.

# RequestHandler Update

As pointed out previously, all requests are handled by one and the same service

  • RequestHandler, which means that it needs to be "informed" how to handle

time frame variable fetch requests.

private async Task<IResponse<ResponseDataBase>> GenerateResponse(IRequest<RequestDataBase> request)
{
    if (request.DataKind == RequestDataKind.Variable)
    {
        if (request.Operation == RequestOperation.GetLastValue)
        {
            return GetLastValueOfVariable(request);
        }
        else if (request.Operation == RequestOperation.GetHistoryValues)
        {
            return await GetHistoryValuesOfVariable(request, new CancellationToken()).ConfigureAwait(false);
        }
        else
        {
            throw new NotSupportedException("Requested operation is not supported.");
        }
    }
    else
    {
        throw new NotSupportedException("Requested data kind is not supported.");
    }
}

Since fetch of history values is asynchronous, the method becomes async and it returns Task<IResponse<ResponseDataBase>> now.

GetHistoryValuesOfVariable uses IRtdbVariablesReader to get the values:

private async Task<Response<GetVariableHistoryValuesResponseData>> GetHistoryValuesOfVariable(IRequest<RequestDataBase> request, CancellationToken cancellationToken)
{
    if (!(request.RequestData is GetVariableHistoryValuesRequestData requestData))
        throw new Exception("Request data is not correct");

    var data = await _rtdbVariableReader.GetValuesAsync(
        requestData.VariableName,
        requestData.StartTimeUtc,
        requestData.EndTimeUtc,
        cancellationToken)
        .ConfigureAwait(false);

    return new Response<GetVariableHistoryValuesResponseData>(
        request.RequestId,
        new GetVariableHistoryValuesResponseData(data));
}

These changes are enough for our module to support the new request type.

# Client Module Update

To see if the updated module works as expected, the client needs to be updated. One method is by adding another periodical request sending operation:

private async void SendHistoryValuesRequestAsync(object source, ElapsedEventArgs e)
{
    _logger?.LogInfo("Sending a new request for history values...");

    var request = new Request<RequestDataBase>(
        _random.Next(1, 1000),
        THIS_MODULE_ID,
        RequestDataKind.Variable,
        RequestOperation.GetHistoryValues,
        new GetVariableHistoryValuesRequestData(
            REQUESTED_VARIABLE,
            DateTime.UtcNow.AddMinutes(-5),
            DateTime.UtcNow));

    await PublishRequest(request).ConfigureAwait(false);
}

The timer needs to be created and initialized similarly as it was done before with last value fetch timer. Additionally a new interval constant was introduced

  • HISTORY_VALUES_REQUEST_SENDING_INTERVAL_MS.

Another way would be to create a new client that only requests historical data and leave the old one unchanged.

That basically finalizes extension process. You can try to run the new versions of the modules and see if the flow is working as it should.

# Final Thoughts

The provided codebase is a good start for real implementation of your custom connector. The things to consider are listed below:

  • request queue is currently unconstrained in terms of its size
  • maintaining RequestHandler class is troublesome as it grows with each new request that it needs to support. Good practice is to create separate services for handling each request type and invoking them by the usage of either reflection or preregistered list of those services in IoC container (recommended) from within RequestHandler. That way, this class would not need to be updated for each new request.
Author: Marcin Jahn
Last updated: 8/5/2021, 5:55:48 AM
Feedback