Link Search Menu Expand Document

Add Configuration

Dec 7 2023 at 12:00 AM

  1. Options in the Receptor Device
  2. Use Case: TCP Server - Receptor Device
    1. Use Case

The Driver SDK allows the user to configure additional Device options in Devices Options files. This section will discuss a simple use case where the Driver Device (in this case the Receptor Device) is configured to listen for connections.

Generally, including configuration involves the following steps:

  • Add relevant Device options in the Device Options Interface (Abstractions) file.
  • Add relevant Device options in the Device Options Class file.
  • Include a Config Exposé in the Device Options Class file.
  • Set relevant properties in the DevicesOptions.json configuration file.

Options in the Receptor Device

Before showing a specific use case, this section will run through the main steps in the process. Consider the example code sample below (to make a Device listen for connections on a certain port):

var client = new TcpListener(Options.Port);
var incomingConnection = await client.AcceptTcpClientAsync();
LogInformation($"New connection from {incomingConnection.Client.RemoteEndpoint?.ToString()}");

Options.Port will import configured options for these commands to execute using the correct port. The process to configure these options will be discussed in the steps below.

1. Device Options Interface (Abstraction)

Navigate to the Device Options Interface (Abstraction) file (found here):

Device Options Interface File

Figure 1 - Device Options Interface (Abstractions) File

In the file, add options for the port. It will look similar to this (example from the SDK template). Add int Port { get; set; }:

using IoTnxt.Raptor.DeviceManager;

namespace IoTnxt.Raptor.Device.driver_demo
{
    /// <summary>
    /// Options for a driver_demo Receptor Device
    /// </summary>
    /// <seealso cref="IRaptorDeviceOptions" />
    /// <seealso cref="IPropertyValidator" />
    public interface Idriver_demoReceptorDeviceOptions: IRaptorDeviceOptions, IPropertyValidator
    {
        Properties

        // TODO: Add properties for this device
        
        /// <summary>
        /// Get or set the Port on which to listen
        /// </summary>
        
        int Port { get; set; }
    }
}

NOTE
Defaults for options can be set in this file. For example, the port number could be set as a default to any given port number (e.g. public int Port { get; set; } = 9007).

2. Devices Options Class

The next step is to implement Interface Options (in this case it is the Port field) in the concrete Device Options Class file.

Navigate to the Devices Options Class file (found here):

Device Options Class File

Figure 2 - Device Options Class File

In the SDK template package, this file should come preloaded with options like Enabled, Debug, DeviceType, GatewayId, and GroupName (see the code sample below):

    public class DriverSdkDemoReceptorDeviceOptions: IDriverSdkDemoReceptorDeviceOptions
    {
        Properties

        /// <inheritdoc />
        [OptionsPropertyMeta("Enabled", "Sets whether this device is enabled.", PropertyMetaType.Boolean, defaultValue: false)]
        public bool Enabled { get; set; }

        /// <inheritdoc />
        [OptionsPropertyMeta("Debug", "Sets whether debug logging is captured.", PropertyMetaType.Boolean, defaultValue: false)]
        public bool Debug { get; set; }

        /// <inheritdoc />
        [OptionsPropertyMeta("Device Type", "The type of the device.", PropertyMetaType.Text)]
        public string DeviceType { get; set; }

        /// <inheritdoc />
        [OptionsPropertyMeta("Gateway Id", "The Gateway ID.  Leave this blank to use the Collector's Gateway ID.", PropertyMetaType.Text)]
        public string GatewayId { get; set; }

        /// <inheritdoc />
        [OptionsPropertyMeta("Group Name", "The name of the group.", PropertyMetaType.Text)]
        public string GroupName { get; set; } = "DriverSdkDemo";

        [OptionsPropertyMeta("Port", "Port on which the server will listen.", "0", "65565", "9007", PropertyMetaType.Numeric)]
        public int Port { get; set; };

        ...
    }

Add the relevant option in this file (as seen in the code sample above). In this case public int Port { get; set; }; needs to be added as a field. It is also important to note that each option comes with an OptionsPropertyMeta Config Exposé. An OptionsPropertyMeta Config Exposé for the Port option will look something like this:

[OptionsPropertyMeta(
    "Port",
    "The port on which the server will listen.",
    PropertyMetaType.Numeric,
    0,
    65565,
    9007
)]

  1. Port - add the property name.
  2. The port on which the server will listen - add a property description.
  3. PropertyMetaType.Numeric - the port number is a numeric data type.
  4. 0 - minimum value.
  5. 65565 - maximum value.
  6. 9007 - default value.

The example code below shows the addition of a Config Exposé to the Port field:

    public class DriverSdkDemoReceptorDeviceOptions: IDriverSdkDemoReceptorDeviceOptions
    {
        ....

        [OptionsPropertyMeta("Port", "Port on which the server will listen.", "0", "65565", "9007", PropertyMetaType.Numeric)]
        public int Port { get; set; };

        ....
    }

Below find the ExampleOptions sample code (for reference). This shows a list of possible options with their OptionsClassMeta Config Exposé options:

using System.Collections.Generic;
using ConfigExposé.Enums;
using IoTnxt.Raptor.Extensions.Configuration;

namespace ConfigExposé.Options;

/// <summary>This is the example options file.</summary>
[OptionsClassMeta(
    "This is the example options name",
    "This is the example options description.")]
public class ExampleOptions
{
    /// <summary>Get or set the value of property 1.</summary>
    /// <remarks>This is an example of an Unknown (any) property.</remarks>
    [OptionsPropertyMeta(
        "Property1",
        "This is an example of an Unknown (any) property.",
        PropertyMetaType.Unknown)]
    public object Property1 { get; set; }

    /// <summary>Get or set the value of property 2.</summary>
    /// <remarks>This is an example of a Numeric (int/long) property.</remarks>
    [OptionsPropertyMeta(
        "Property2",
        "This is an example of a Numeric (int/long) property.",
        PropertyMetaType.Numeric,
        0,
        100,
        25)]
    public int Property2 { get; set; }

    /// <summary>Get or set the value of property 3.</summary>
    /// <remarks>This is an example of a Decimal (double/float/decimal) property.</remarks>
    [OptionsPropertyMeta(
        "Property3",
        "This is an example of a Decimal (double/float/decimal) property.",
        PropertyMetaType.Decimal,
        0.0,
        100.0,
        25.0)]
    public decimal Property3 { get; set; }

    /// <summary>Get or set the value of property 4.</summary>
    /// <remarks>This is an example of an Email (string) property.</remarks>
    [OptionsPropertyMeta(
        "Property4",
        "This is an example of an Email (string) property.",
        PropertyMetaType.Email,
        0,
        100,
        "[email protected]",
        regEx:@"/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/")]
    public string Property4 { get; set; }

    /// <summary>Get or set the value of property 5.</summary>
    /// <remarks>This is an example of a Password (string) property.</remarks>
    [OptionsPropertyMeta(
        "Property5",
        "This is an example of a Password (string) property.",
        PropertyMetaType.Password,
        0,
        100,
        "abc123",
        regEx: @"^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$",
        regExHint: "Minimum eight characters, at least one letter and one number:"
        )]
    public string Property5 { get; set; }

    /// <summary>Get or set the value of property 6.</summary>
    /// <remarks>This is an example of a IpAddressV4 (string) property.</remarks>
    [OptionsPropertyMeta(
        "Property6",
        "This is an example of a IpAddressV4 (string) property.",
        PropertyMetaType.IpAddressV4,
        7,
        15,
        "127.0.0.1")]
    public string Property6 { get; set; }

    /// <summary>Get or set the value of property 7.</summary>
    /// <remarks>This is an example of a IpAddressV6 (string) property.</remarks>
    [OptionsPropertyMeta(
        "Property7",
        "This is an example of a IpAddressV6 (string) property.",
        PropertyMetaType.IpAddressV6,
        null,
        null,
        "127.0.0.1")]
    public string Property7 { get; set; }

    /// <summary>Get or set the value of property 8.</summary>
    /// <remarks>This is an example of a Hostname (string) property.</remarks>
    [OptionsPropertyMeta(
        "Property8",
        "This is an example of a Hostname (string) property.",
        PropertyMetaType.Hostname,
        0,
        100,
        "server1.iotnxt.io")]
    public string Property8 { get; set; }

    /// <summary>Get or set the value of property 9.</summary>
    /// <remarks>This is an example of a Text (string) property.</remarks>
    [OptionsPropertyMeta(
        "Property9",
        "This is an example of a Text (string) property.",
        PropertyMetaType.Text,
        0,
        250,
        "hello world")]
    public string Property9 { get; set; }

    /// <summary>Get or set the value of property 10.</summary>
    /// <remarks>This is an example of a Boolean (bool) property.</remarks>
    [OptionsPropertyMeta(
        "Property10",
        "This is an example of a Boolean (bool) property.",
        PropertyMetaType.Boolean,
        defaultValue: true)]
    public bool Property10 { get; set; }

    /// <summary>Get or set the value of property 11.</summary>
    /// <remarks>This is an example of a UrlPath (string) property.</remarks>
    [OptionsPropertyMeta(
        "Property11",
        "This is an example of a UrlPath (string) property.",
        PropertyMetaType.UrlPath,
        0,
        200,
        "https://iotnxt.com")]
    public string Property11 { get; set; }

    /// <summary>Get or set the value of property 12.</summary>
    /// <remarks>This is an example of an Enumeration (enum) property.</remarks>
    [OptionsPropertyMeta(
        "Property12",
        "This is an example of an Enumeration (enum) property.",
        PropertyMetaType.Enumeration,
        defaultValue: nameof(ExampleType.Type1),
        enumRange: typeof(ExampleType))]
    public string Property12 { get; set; }

    /// <summary>Get or set the value of property 13.</summary>
    /// <remarks>This is an example of a KeyValuePair (dictionary) property.</remarks>
    [OptionsPropertyMeta(
        "Property13",
        "This is an example of a KeyValuePair (dictionary) property.",
        PropertyMetaType.Object,
        isDictionary: true,
        dictionaryKeyType: PropertyMetaType.Text,
        nestedOptionsType: typeof(string))]
    public IDictionary<string, string> Property13 { get; set; }

    /// <summary>Get or set the value of property 14.</summary>
    /// <remarks>This is an example of an Collection (list) property.</remarks>
    [OptionsPropertyMeta(
        "Property14",
        "This is an example of an Collection (list) property.",
        PropertyMetaType.Text,
        isList: true)]
    public IList<string> Property14 { get; set; }

    /// <summary>Get or set the value of property 15.</summary>
    /// <remarks>This is an example of an Object (object) property with sub-meta.</remarks>
    [OptionsPropertyMeta(
        "Property15",
        "This is an example of an Object (object) property.",
        PropertyMetaType.Object,
        nestedOptionsType: typeof(SubExampleOptions1))]
    public SubExampleOptions1 Property15 { get; set; }

    // /// <summary>Get or set the value of property 16.</summary>
    // /// <remarks>This is an example of an Object (object) property with poly-meta.</remarks>
    // [OptionsPropertyMeta(
    //     "Property16",
    //     "This is an example of an Object (object) property.",
    //     PropertyMetaType.Object)]
    // public ISubExampleOptions Property16 { get; set; }
    //
    // /// <summary>Get or set the value of property 17.</summary>
    // /// <remarks>This is an example of an Object (object) property with poly-meta.</remarks>
    // [OptionsPropertyMeta(
    //     "Property17",
    //     "This is an example of an Object (object) property.",
    //     PropertyMetaType.Object)]
    // public ISubExampleOptions Property17 { get; set; }
    
    /*
    ██     ██  █████  ██████  ███    ██ ██ ███    ██  ██████  
    ██     ██ ██   ██ ██   ██ ████   ██ ██ ████   ██ ██       
    ██  █  ██ ███████ ██████  ██ ██  ██ ██ ██ ██  ██ ██   ███ 
    ██ ███ ██ ██   ██ ██   ██ ██  ██ ██ ██ ██  ██ ██ ██    ██ 
     ███ ███  ██   ██ ██   ██ ██   ████ ██ ██   ████  ██████
 
    PLEASE DO NOT CREATE AN OPTIONS PROPERTY LIKE THIS, IT WILL 100% CRASH THE SERVICE.
    This is due to the fact that it's building meta of a type infinitely for it's child.
    
    /// <summary>Get or set the value of property 999.</summary>
    /// <remarks>This is an example of an Object (object) property with sub-meta self referencing itself.</remarks>
    [OptionsPropertyMeta(
        "Property999",
        "This is an example of an Object (object) property with sub-meta self referencing itself.",
        PropertyMetaType.Object,
        nestedOptionsType: typeof(ExampleOptions))]
    public ExampleOptions Property999 { get; set; }
    
    */
}

NOTE
To ensure the correct configuration is added to the Config Exposé, query the relevant API to fetch a sample configuration. This will fetch the OptionsPropertyMeta object which provides the appropriate values, including relevant default values. It is highly recommended that this be done to ensure the correct setup for Driver options.

3. DevicesOptions.json “Devices” section

The last step is to ensure that options in the “Devices” section of the DevicesOptions.json file correspond with options added in the Device Options Interface and Class files. In this case it is the Port number configured in the previous two steps.

Navigate to the DevicesOptions.json file:

DeviceOptions

Figure 3 - DevicesOptions.json

In this file (in the “Devices” section), set the relevant property. In this case Port is added, with the correct port number:

"Devices": {
    "DemoReceptorDevice": {
      "Enabled": true,
      "DeviceType": "Receptordriver_demo",
      "Debug": true,
      "OtherProperties": "here",
      "Port": 9007
    }
}

Use Case: TCP Server - Receptor Device

In this use case, the Driver is written to listen to TCP connections. In the Device’s methods, options that need to be added are the Server URL Address where the requests need to be sent, and the Port number on which the Device needs to listen.

Use Case

Consider the following use case, where the method contained in the Device’s OnStartAsync does the following:

  • Starts a TCP Server.
  • Logs a message that “My Custom Driver Has Started”.
  • Includes callbacks for Connecting (OnConnectAsync), Disconnecting (OnDisconnectAsync) and Collecting Data (OnDataReceived).
  • Logs a message that says “Started listening for connections on {Options.Address}: {Options.PortNo}”.
protected override async Task OnStartAsync() 
{
    LogWarning("My Custom Driver Has Started");
    _tcpServer.OnConnectAsync = OnConnectAsync;
    _tcpServer.OnDisconnectAsync = OnDisconnectAsync;
    _tcpServer.OnDataReceivedAsync = OnDataReceived;

    await _tcpServer.StartAsync(Options);

    LogInformation($"Started listening for connections on {Options.Address}:{Options.PortNo}");
}

private Task OnDataReceived(IRaptorTcpSession arg1, IRaptorTcpSessionData arg2)
{
    LogInformation($"Messages received from {arg1.RemoteAddress}");

    var payload = arg2.RemoveData(arg2.Data.Length);
    var data = Encoding.UTF8.GetString(payload);
    LogInformation($"Data received: {data}");
    return Task.CompletedTask;
}

private Task OnDisconnectAsync(IRaptorTcpSession arg)
{
    _connectedDeviceCount.Dec();
    LogInformation($"Dropped connection from {arg.RemoteAddress}");
    return Task.CompletedTask;
}

private Task OnConnectAsync(IRaptorTcpSession arg)
{
    _connectedDeviceCount.Inc();
    LogInformation($"New connection from {arg.RemoteAddress}");
    return Task.CompletedTask;
}

In the steps below, the guide will look at how to configure the Address and PortNo Options found in this code sample.

NOTE
This use case focuses on steps to configure the Options found in the code sample above. It assumes the user has already injected IRaptorTcpServer and IMetricsFactory in the constructor, included InitializeMetrics and added _connectedDeviceCount and _tcpServer to the Private Members section. For more information about how to create metrics, go to the Add Metrics section in this documentation.

A more comprehensive code sample for TcpService configuration can be found in the sample below. This code sample is provided for reference and includes possible configurations for methods related to the TcpService:

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IoTnxt.Raptor.TcpServer;
using IoTnxt.Raptor.Transient;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Server;

/// <summary>
/// Illustrates how to start a TCP Service and process incoming data
/// </summary>
public class TcpService: ITcpService, IHostedService
{
    private readonly ILogger _logger;
    private readonly IOptionsMonitor<RaptorTcpServerOptions> _tcpServerOptions;
    private readonly ITransientDisposableFactory<IRaptorTcpServer> _tcpFactory;
    private IRaptorTcpServer _tcpServer;

    /// <summary>
    /// Creates a new instance of a <see cref="TcpService"/>
    /// </summary>
    /// <param name="logger">The logger</param>
    /// <param name="tcpServerOptions">TCP Server options</param>
    /// <param name="tcpFactory">The TCP Server factory</param>
    public TcpService(ILogger<TcpService> logger,
        IOptionsMonitor<RaptorTcpServerOptions> tcpServerOptions,
        ITransientDisposableFactory<IRaptorTcpServer> tcpFactory)
    {
        _logger = logger;
        _tcpServerOptions = tcpServerOptions;
        _tcpFactory = tcpFactory;
    }

    /// <inheritdoc />
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting TCP Server");
            
        // Create server instance
        _tcpServer = _tcpFactory.CreateNonTrackedTransient();
        // Setup callbacks
        _tcpServer.OnConnectAsync = OnConnectAsync;
        _tcpServer.OnDisconnectAsync = OnDisconnectAsync;
        _tcpServer.OnDataReceivedAsync = OnDataReceived;
            
        // Start server
        await _tcpServer.StartAsync(_tcpServerOptions.CurrentValue);
    }

    /// <inheritdoc />
    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Stopping TCP Server");
        await _tcpServer.StopAsync();
        _tcpServer.Dispose();
    }

    // TCP Server callbacks
        
    private Task OnConnectAsync(IRaptorTcpSession session)
    {
        _logger.LogInformation($"Client Connected: Id={session.Id}");
        return Task.CompletedTask;
    }

    private Task OnDisconnectAsync(IRaptorTcpSession session)
    {
        _logger.LogInformation($"Client Disconnected: Id={session.Id}");
        return Task.CompletedTask;
    }

    private async Task OnDataReceived(IRaptorTcpSession session, IRaptorTcpSessionData data)
    {
        var payload = data.RemoveData(data.Data.Length);
        _logger.LogInformation($"Client Sent Data: Id={session.Id}, Payload={Encoding.UTF8.GetString(payload)}");
            
        // We are just echoing the data back to the client as a response
        await session.SendDataAsync(payload);
    }
}

Configuring Address and PortNo Options

In order for the method to know which Address and PortNo to include, the following needs to be added:

  • In the Device Options Interface file, add IRaptorTcpServerOptions. This injects predefined options that can be implemented in the Class.
  • In the Device Options Class file, implement the IRaptorTcpServerOptions.
  • Set options in the DevicesOptions.json “Devices” section.

1. Adding IRaptorTcpServerOptions:

In the Device Options Interface file, include IRaptorTcpServerOptions:

using IoTnxt.Raptor.DeviceManager;
using IoTnxt.Raptor.TcpServer;

namespace IoTnxt.Raptor.Device.driver_demo
{
    /// <summary>
    /// Options for a driver_demo Receptor Device
    /// </summary>
    /// <seealso cref="IRaptorDeviceOptions" />
    /// <seealso cref="IPropertyValidator" />
    public interface I_driver_demoReceptorDeviceOptions: IRaptorDeviceOptions, IPropertyValidator, IRaptorTcpServerOptions
    {
        Properties

        // TODO: Add properties for this device

    }
}

This will let the user implement a number of predefined options in the Device Options Class file.

2. Device Options Class - adding options:

In the Device Options Class file, add using IoTnxt.Raptor.TcpServer and implement the options. IRaptorTcpServerOptions contains many options, including Address and PortNo. For the purpose of this use case, we will focus on those two options and how to make sure the Device picks up the configured options in the relevant method.

An example that includes all TcpServerOptions would look like this:

using IoTnxt.Raptor.TcpServer;

namespace Server;

public class TcpServerOptions: IRaptorTcpServerOptions
{
    public string Address { get; set; }
    public int PortNo { get; set; } = 50_000;
    public EncryptionType Encryption { get; set; } = EncryptionType.None;
    public TlsSettings TlsVersion { get; set; } = TlsSettings.All;
    public string CertificateSubjectName { get; set; }
    public bool ValidateClientCertificate { get; set; }
    public string CertificatePath { get; set; }
    public string CertificatePassword { get; set; }
    public int MaxConnections { get; set; }
    public int RetryDelay { get; set; }
    public int InitializeTimeout { get; set; }
    public int CertificateValidationTimeout { get; set; }
    public int Backlog { get; set; }
    public int ReadBufferSize { get; set; }
    public DroppedClientDetection DroppedClientDetection { get; set; }
    public int DroppedClientTimeoutSecs { get; set; }
    public int NoDataTimeout { get; set; }
    public bool Debug { get; set; }
        
    public void Validate() { }
}

For this use case, add Address and PortNo to the Class options:

using IoTnxt.Raptor.TcpServer;

namespace DemoApp;

public class DemoDeviceOptions : IDemoDeviceOptions
{
    public string Address { get; set; }
    public int PortNo { get; set; }
}

NOTE
If a Config Exposé is necessary, query the API to fetch a sample configuration to ensure the correct configuration is added. Querying the API will fetch the OptionsPropertyMeta object which provides the appropriate values, including relevant default values. It is highly recommend that this be done to ensure the correct setup for Driver options.

3. DevicesOptions.json - setting properties

In the “Devices” section of the DevicesOptions.json file, set Address and PortNo under the relevant Device.

"Devices" : {
    "DemoDevice" : {
        "DeviceType" : "<DeviceType>",
        "Enabled" : true,
        "Address" : "0.0.0.0",
        "PortNo" : 8000
    }
}

Now the Address and PortNo values have been set and will be implemented in the Device’s OnStartAsync method.