Show / Hide Table of Contents

Tutorials

The below tutorials discuss how to use the various components of the Agora Edge Apps SDK. It provides a section on Getting Started and then tutorials on Logging, Configuration, Broker and BusClient.

Getting Started

Requirements

You can use the following methods to create an Agora Edge App. These are provided in separate documents. Make sure all the following tools installed on the developer machine.

  1. Using Visual Studio (Community/Professional/Enterprise)
  2. Using Visual Studio Code
  3. Using Command Line (Windows/Linux)

Logging Tutorial

By the end of the logging tutorial you should feel comfortable logging messages and understand logging level using the Agora Edge App SDK. There is nothing extremely special about the logging capability of the SDK compared to open source loggers, but it (like all others) has its own configuration settings which is integrally tied to the SDK's configuration capabilities.

Logging is required by the SDK to allow the SDK the ability to log information about itself.

Before you begin

You should have completed the Getting Started section to integrate the AgoraIoT.Edge.App.SDK into your project before starting and have a project ready to use with the tutorial.

Logging with String Extensions

In general, most logging can be accomplished just using String Extensions for Logging, which is what this tutorial focuses on.

From the IDE, open Program.cs within the Console application created in the Getting Started tutorial and copy the code below into it:

  • NET
  • Python
public class Program
{
    public static void Main(string[] args)
    {
        "Starting".LogHeading();
        "Hello from Module!".LogWarn();
        $"The current local time is {DateTime.Now}.".LogInfo();
        "A debug message".LogDebug();
        "A trace message".LogTrace();
        "Stopping".LogHeading();
    }
}
from agoraiot import logger
import time
logger.heading("Starting")
logger.warn("Hello from Module!")
logger.info(f"The current local time is {time.now()}.")
logger.debug("A debug message")
logger.trace("A trace message")
logger.heading("Stopping")

Run the application. You should see the output similar to what is below in the application's Console window.

-------------------------------------------------------------------------------
(33)                                                                   Starting
-------------------------------------------------------------------------------
W(36) - Hello from Module!
I(36) - The current local time is 5/3/2022 5:17:13 PM.
-------------------------------------------------------------------------------
(37)                                                                   Stopping
-------------------------------------------------------------------------------

Within the log, each message begins with the first letter of the LogLevel followed by the number of milliseconds since the application started in parentheses. If the LogLevel of the message being written is LogLevel.Info, the message will not contain the file, line number, and member name.

Notice that the Debug and Trace messages did not appear. This is because the Logging Level defaults to LogLevel.Info and above. To adjust the level of logging messages output by the Logger for troubleshooting and development use the AEA2:LogLevel configuration setting. Assigning LogLevel sets the minimum level to be output, which corresponds to the LogLevel enumeration.

  • Trace (0): Finest level of logging
  • Debug (1): Used for debugging - specifically log values that should not go to Release
  • Info (2): General logging information - filename, line number, and method are not included
  • Warn (3): Used to indicate something is not happening as expected
  • Error (4): Indicates an error occurred, but not fatally. Errors are used to indicate that the configuration error or unexpected software problem. It should not be used to log problems with the workflow or process that the application is performing.
  • Fatal (5): Generally used just before the application is giving up, just before it stops.
  • Off (6): Turns off all logging.

Additionally, the level of logging can be adjusted using configuration and by setting the level directly on the logger.

Configuration Settings affecting Logging using 'AEA.json' configuration file.

{
    ...
    "AEA2": {
        "LogLevel": "Debug"
    }
    ...
}

Programmatically, the default logging level can be overridden calling the appropriate method on the logger.

Important

Setting LogLevel programmatically via the logger API can have unexpected results because Configuration changes will cause the LogLevel to be reset. To override any configuration setting, use configuration overrides.

Setting log level using the logger

  • NET
  • Python
using static Agora.SDK;

Log.SetLevel(Agora.Logging.LogLevel.Debug);
from agoraiot import logger, LogLevel

logger.set_level(LogLevel.DEBUG)

Setting log level using configuration overrides

  • NET
  • Python
using static Agora.SDK;

Config.Overrides["AEA2:LogLevel"] = "Debug";
Config.Build();
from agoraiot import config

config.overrides["AEA2:LogLevel"] = "Debug"
config.build();

Configuration Tutorial

By the end of the configuration tutorial you should feel comfortable using the SDK Configuration capabilities and have an understanding of the dynamic nature of configuration which is an integral part for making your application configurable and modifiable - even while the application is running.

Before you begin

You should have completed the Getting Started tutorial to integrate the Agora.Edge SDK into your project before starting and have a project ready to use with the tutorial.

Adding 'AEA.json' and Naming your App

The default name for your application is the Entry Assembly Name. This Name can be overridden using Agora Edge App Configuration, which is very useful when configuring the application to interact with other applications.

Important

The Application Name (Config["Name"]) is used as the MQTT Client identifier when routing messages.

To begin, copy the following code into the Program.cs of your application.

  • NET
  • Python
using static Agora.SDK;

public class Program
{
    public static void Main(string[] args)
    {
        "Starting".LogHeading();
        $"Hello from {Config["Name"]}!".LogInfo();
        "Stopping".LogHeading();
    }
}
from agoraiot import config, logger

logger.heading("Starting")
logger.info(f"Hello from {config['Name']}!")
logger.heading("Stopping")

Running the application will produce an output similar to:

-------------------------------------------------------------------------------
(33)                                                                   Starting
-------------------------------------------------------------------------------
I(36) - Hello from Module!
-------------------------------------------------------------------------------
(37)                                                                   Stopping
-------------------------------------------------------------------------------

To override the name of the application:

  1. Add a json file to the project called AEA.json.
  2. For .NET, in the Properties of the file, for 'Copy to Output Directory' select Copy if newer.

Configure AEA.json

If using VSCode , add <None Update="AEA.json" CopyToOutputDirectory="PreserveNewest" /> in the csproj file

  <ItemGroup>
    <PackageReference Include="AgoraIoT.Edge.App.SDK" Version="2.0.0" />
    <None Update="AEA.json" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>

To set the Name, open the AEA.json file and add the following:

AEA.json configuration file:

{
    "Name": "MyApp"
}

Multiple Configuration Settings

Part I: Nested Settings

An AEA.json file can contain nested settings.

Copy the contents below into your AEA.json file of your application.

{
    "Name": "MyApp",
    "MyApp": {
        "Setting1": "Value",
        "Setting2": ["An", "Array", "of", "Values"]
    }
}

The following codes shows how to access the settings.

  • NET
  • Python

Program.cs:

using System.Text;
using static Agora.SDK;

public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine($"The name of my app is `{Config["Name"]}'");
        Console.WriteLine($"Setting1 = `{Config["MyApp:Setting1"]}'");

        /* Accessing a setting with an array */
        var settings = Config.GetSection("MyApp:Setting2").GetChildren().AsEnumerable();

        Console.Write("Setting2 contains `");
        foreach (var s in settings)
            Console.Write(s.Value + ' ');

        Console.WriteLine("'.");
    }
}

app.py:

from agoraiot import logger, config

print(f"The name of my app is '{config['Name']}'")
print(f"Setting 1 = '{config['MyApp:Setting1']}'")

settings = config['MyApp:Setting2']

print("Setting 2 contains '", end="")
for setting in settings:
    print(setting + ' ', end="")

print("'.")

Output:

The name of my app is `MyApp'
Setting1 = `Value'
Setting2 contains `An Array of Values '.

Part II: Using Alternate, Key-Per-File, Environment Variable, and Command-Line Configuration

The purpose of the alternate and Key-Per-File configuration is to allow configuration of the application once it is deployed as a container. Typically, the container would be installed on a Gateway, and AgoraOps would configure the application as needed for the problem being addressed by the application.

The SDK looks for alternate configuration files in the config folder, which AgoraOps configures to be a shared volume, for example, using the command:

docker run -v /var/app-configs/MyApp/config:/MyAppDir/config MyApp

For .NET applications, the config folder is in the same folder as the starting application.

For Python, the 'config' folder is in the same folder where the python myapp.py is executed.

The config folder can contain the alternate configuration, config/AEA.json and a folder with key-per-file settings config/keys/*.

When running as a container, the SDK loads the configuration components in the following order:

  1. Default Settings: Set within the application.
  2. The Primary Config: AEA.json of the application
  3. Environment Variables: 'AEA__*' variables.
  4. Command-line Arguments: ex. App -x=123 where -x is a switch map to settings within the application
  5. Alternate Config: config/AEA.json
  6. Key-Per-File: config/keys/*
  7. Override Settings: Set within the application.

To see how these work together, perform the following steps:

  • NET
  • Python
  1. Using the same Program.cs from the pervious tutorial go to the executing assembly folder. Generally this will be in the bin/Debug/net6.0 folder of the project.
  2. Add a new directory called config.
  3. Add a new alternate configuration file called config/AEA.json with the following contents and run the application.
{
    "MyApp": {
        "Setting1": "New Value for Setting 1"
    }
}

File structure:

bin\
    Debug\
          net6.0\
                config\AEA.json
  1. Using the same python code from the pervious tutorial go to the folder.
  2. Add a new directory called config.
  3. Add a new alternate configuration file called config/AEA.json with the following contents and run the application.
{
    "MyApp": {
        "Setting1": "New Value for Setting 1"
    }
}

File structure:

app.py
    config/
        AEA.json

Output:

The name of my app is `MyApp'
Setting1 = `New Value for Setting 1'
Setting2 contains `An Array of Values '.

You can also modify the configuration using environment variables, but since you cannot use ':' character in environment variable or in filenames, substitute '__' for the colons. Additionally, for environment variable to be recognized by the SDK, AEA__ must be prepended to the environment variables.

  1. Open a command window.
  2. Change directory / drive to the bin/Debug/net6.0 of the project.
  3. Run the command: set AEA__MyApp__Setting1=Environment Variable Setting
  4. Run the application.

Output:

The name of my app is `MyApp'
Setting1 = `Environment Variable Setting'
Setting2 contains `An Array of Values '.

To modify the configuration using command line parameters execute one can use several methods. The following are equivalent methods that can be used as command-line arguments to modify MyApp:Setting1:

> <appName.exe> MyApp:Setting1=123
> <appName.exe> /MyApp:Setting1=123
> <appName.exe> --MyApp:Setting1=123
> <appName.exe> /MyApp:Setting1 123
> <appName.exe> --MyApp:Setting1 =123
> <appName.exe> --myapp:setting1 =123
Note

For .NET SDK, it is recommended by Microsoft not to mix multiple methods together.

Note

Settings (key names) are case insensitive.

Important

For Python SDK, the use of a switch map is required.

Executing any of the above will give:

The name of my app is `MyApp'
Setting1 = `123'
Setting2 contains `An Array of Values '.

Below shows how to modify or add an element to the array of MyApp:Setting2:

> set AEA__myapp__setting2__1=List
> <appName.exe>
The name of my app is `MyApp'
Setting1 = `Environment Variable Setting'
Setting2 contains `An List of Values '.

> <appName.exe> myapp:setting2:0=A
The name of my app is `MyApp'
Setting1 = `Environment Variable Setting'
Setting2 contains `A List of Values '.

Key-Per-File works very much like modifying environment variables.

Caution

Common Misunderstanding: When using Key-Per-File, the contents of the file is generally not JSON. The entire contents of the file is the value. I.e. The file name is the KEY and the file contents is the VALUE.

Create the file: config/keys/myapp__setting2__3 with the following contents:

Settings

Run the executable again and you should see:

> <appName.exe>
The name of my app is `MyApp'
Setting1 = `Environment Variable Setting'
Setting2 contains `An List of Settings
 '.
Note

Make a note of the addition of the EOL character, which appears to be a bug in the Microsoft.Configuration.Extensions.

Part III: Responding To Configuration Changes

While your application runs at the edge, it is likely that you will want to make it respond to configuration changes. There are two ways to do this:

  1. The first method is to monitor for all configuration changes using the Config Reload Token which is part of the Microsoft.Configuration.Extensions.
  2. The second method is to use an Agora.ObservableSetting.

Both methods are illustrated as below:

AEA.json:

{
  "Name": "ConfigurationTutorial",
  "Author": "Original Author",
  "Authors": [ "Author1", "Author2" ]
}
  • NET
  • Python

Program.cs:

using static Agora.SDK;

public class Program
{
    static public void Main()
    {
        "Starting".LogHeading();

        WriteAuthor();

        // Method 1
        //Config.GetReloadToken().RegisterChangeCallback(InvokeChange, null);
        //"If you modify any setting you will get a ReloadTokenChange".Cout();

        // Method 2
        //ObservableSetting.Get("Author").PropertyChanged += AuthorObservableSettingChanged;
        //$"If you modify the 'Author' setting, you will get an ObservableSetting notifications.".Cout();

        Console.ReadLine();

        "Stopping".LogHeading();
    }

    private static void WriteAuthor() => $"Author is set to `{Config["Author"]}`".Cout();

    private static void AuthorObservableSettingChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        WriteAuthor();
    }

    private static void InvokeChange(object o)
    {
        "Configuration Reloaded:".Cout();
        WriteAuthor();

        // You must re-register the change callback as the Configuration.ReloadToken has changed
        Config.GetReloadToken().RegisterChangeCallback(InvokeChange, null);
    }
}
Tip

When you use the method 1, you will notice each time you make a configuration change, you will get two callbacks. This is a known issue in the Microsoft.Extensions.Configuration and has been attributed to the system having to handle multiple operating systems which may or may not have file change callbacks and need to use file polling instead.

from agoraiot import *

def handle_config_change():
    print( "Configuration Reloaded" )
    write_author()

def author_setting_change(payload):
    write_author()

def write_author():
    print( f"Author is set to `{config["Author"]}`")


logger.heading("Starting")

write_author()

# Method 1
# config.observer_config(handle_config_change)
# print("If you modify any setting you will get a ReloadTokenChange")

# Method 2
# config.observe("Author", author_setting_change)
# print("If you modify the 'Author' setting, you will get a notification)

input("Waiting")

logger.heading("Stopping")

Both methods of handling configuration change may be used, even at the same time. If you uncomment the lines after each // Method you can see what happens when you modify settings using the config/AEA.json file and the key-per-files such as config/keys/authors or config/keys/authors__0.

Printing the Full Configuration

Sometimes it is necessary to understand the full configuration to assist in troubleshooting issues. Below shows how to use the SDK to write the configuration to logger as Info.

  • NET
  • Python

Microsoft.Extensions.Configuration does not currently have the ability to serialize an IConfiguration, but the Agora Edge Apps SDK provides an extension method to do this.

var json = Config.SerializeToJson();
if ( json != null )
    json.ToJsonString(new JsonSerializerOptions() { WriteIndented = true }).LogInfo();
logger.info( json.dumps(config.get_dict(), indent=4) )

Broker Tutorial

Agora Edge App Message Broker Tutorial

The examples in the section are pending inclusion of container images in a public docker registry. You should be able to get the overview of how this should work by just reading the text below until the container images are made publically available.

The Agora Edge App Message Broker can be run as a container on the gateway or in a development environment capable of running Linux containers.

To learn how the broker works, you can pull the container using the following command:

Note

Below commands will be usefull once container images are made available on Public Docker Registry. Internal/Private Container Images are available on Agora Private Container Registry.

docker pull agoraiot/AEA2-Broker

To run the broker, execute the following command:

docker run -rm -it -p 707:707 -v <configurationDir>:/var agoraiot/AEA2-Broker

where:

  • -rm : removes container after it exits
  • -it : run in interactive mode
  • -p : maps host port (707) to container port (707)
  • -v : maps <configurationDir> to container's /var folder, which is used for configuration

The configuration folder, <configurationDir>, can be a folder on the gateway or your development machine. For Windows, include the drive letter as shown:

Shared Volume (Windows): N:\repos\test\var

docker run -rm -it -p 707:707 -v /n/repos/test/var:/var agoraiot/AEA2-Broker

If you install Mosquitto, you can use the mosquitto clients to work with the Agora Edge Apps Broker.

With the broker running in a container, start a command window, and change directory to the mosquitto folder.

Note

The location of the mosquitto_sub and mosquitto_pub varies by operating system. For Windows this is likely to be C:\Program Files\Mosquitto for x64 install or C:\Program Files (x86)\Mosquitto for x86 install.

Start the Mosquitto subscriber to listen for DataIn messages:

mosquitto_sub -p 707 -i Module1 -t DataIn

where:

  • '-p 707' - the port for the broker
  • '-t DataIn' - the topic being subscribed to
  • '-i Module1' - the name of the client

To publish using Mosquitto publish to DataOut:

mosquitto_pub -p 707 -t DataOut -i Module2 -m Hello

where:

  • '-p 707' - the port for the broker. The default port is 707.
  • '-t DataOut' - the topic you are publishing to. Internally, the broker converts this to 'Module2\DataOut'
  • '-i Module2' - the name of the client
  • '-m Hello' - the message

When you run the mosquitto_pub, you will notice that nothing happens, i.e. the subscriber did not receive the message.

Note

Typically on an MQTT broker if one publishes data on a particular topic, it is received by someone listening to that topic. For example, publish a message to the Data topic then a subscriber of the topic, Data, will receive the message. And if you run the subscriber and publisher this way, directly against mosquitto, you would get that result.

For Agora Edge Apps, we do not want all data to flow between all applications, nor do we want each application to be concerned about where its data is going, i.e. configuring each application individually to publish data to topics or applications it knows nothing about. What we do instead is route the messages.

For the above mosquitto pub/sub to work, we need to configure the route in the Agora Edge App Broker.

To add a route create the <configurationDir>/AEA2-Broker/config/routes.json file with the following contents:

{
    "Routes": [
        "/Module2/DataOut TO /Module1/DataIn"
    ]
}

if you run again the mosquitto_pub command above, you should see the message was received by the subscriber.

Hello

Note that a few things happened.

  • The AEA2-Broker is running as a container and is using port 707.
  • The AEA2-Broker configuration folder is mapped to the <configurationDir> which is actively monitored.
  • When you create the file routes.json, the broker will see the file and load it.
  • When messages the DataOut message is received by the broker it uses the route specified in routes.json to convert it. It does this by using the client name of the sender and constructing the topic /Module2/DataOut which it then looks up in the routing table to convert to /Module1/DataIn, which the broker publishes.

Sending Data using Messaging

This tutorial instructs on how to send data to a Message Broker, in this case Mosquitto. Architecturally, the system being constructed in this Tutorial is shown below:

Sending Data Tutorial - Architecture

Normally, you will run your applications using the Agora Message Broker, which supports routing, but this tutorial is illustrating that underneath only MQTT is being used to communicate between the Apps and the purpose of the Broker, which should be clear if you consider that many senders and receivers are likely to be present when running on the edge.

To send messages using the Agora.SDK BusClient the BusClient is used. Ex. Bus.SendData(...).

To get started, from the IDE, open Program.cs within the Console application created in the Getting Started tutorial and copy the code below into Program.cs.

using static Agora.SDK;

internal class Program
{
    private static void Main(string[] args)
    {
        "Starting".LogHeading();

        Bus.Connect(90000);

        Agora.Edge.Messages.DataReport.IoDataReportMsg msg = new();
        msg.Add("999", "HKLD", new()
        {
            value = 10,
            timestamp = (long)AgoraTimeStamp
        });

        Bus.SendData(msg);

        Console.ReadLine();

        "Stopping".LogHeading();
    }
}

Add an AEA.json to your project to configure the application. In this configuration file, you will specify the default port for the Mosquitto broker, 1883. Copy the contents below into the configuration file:

AEA.json:

{
  "Name": "Sender",
  "AEA2": {
    "BusClient": {
      "Port": 1883
    }
  }
}

Make sure the AEA.json is set to Copy to Output Directory. For Visual Studio, you set this as shown below:

AEA.json - Copy if newer

Launch Mosquitto from the command line if it is not already running:

C:\Program Files\mosquitto> mosquitto

In another command window, launch a Mosquitto subscriber for the topic 'DataOut', which the Sender will be sending messages to.

C:\Program Files\mosquitto> mosquitto_sub -t DataOut

Run the Sender application and you should see the following from your Mosquitto Subscriber:

C:\Program Files\mosquitto>mosquitto_sub -t DataOut
{
  "header": {
    "SrcModule": "Sender",
    "MessageType": "IODataReport",
    "ConfigVersion": 1,
    "MessageID": 252711801,
    "TimeStamp": 1666266380177
  },
  "device": [
    {
      "id": "18",
      "tags": {
        "HKLD_m": {
          "value": 10,
          "timestamp": 1666266380177,
          "quality_code": 0
        }
      }
    }
  ]
}

Receiving Data using Messaging

The Sender application in the last tutorial is sending data to the broker using the DataOut topic. However, an Agora Edge App does not receive DataOut messages, it receives DataIn messages. One of the purposes of the Agora Broker which runs on the edge and in your development environment is to transfer DataOut messages to DataIn messages as defined in the AEABroker's routes.json configuration file. This assures that applications don't have to know about one another prior to deployment and centralizes the "wiring" of data paths between one another to a single location, while keeping messaging as generic as possible from the client's point-of-view.

For the Receiving Data tutorial, we again are not going to use the AEABroker, but instead will use mosquitto.

To start, copy the following code into your Program.cs:

using static Agora.SDK;

internal class Program
{
    private static void Main()
    {
        "Starting".LogHeading();

        Bus.Connect(90000);

        while(true)
        {
            if(Bus.Messages.HasDataInMessages)
            {
                var data = Bus.Messages.GetDataInMessages();
                foreach(var message in data)
                {
                    foreach(var device in message.device)
                    {
                        foreach(var tag in device.tags)
                        {
                            var dt = Agora.Utilities.Time.GetDateTimeFromAgoraTimeStamp((double)tag.Value.timestamp!);
                            $"{tag.Key} = {tag.Value.value} @ {dt}".LogInfo();
                        }
                    }
                }
            }
            Thread.Sleep(100);
        }
    }
}

Add an AEA.json to your project to configure the Name of the application. Copy the contents below into the configuration file:

AEA.json for `Receiver':

{
  "Name": "Receiver",
  "AEA2": {
    "BusClient": {
      "Port": 1883,
      "UseDataIn":  true
    }
  }
}

You can see in the configuration we have set "UseDataIn": true. This tells the SDK that it should listen for DataIn messages.

As you did in the Sending Data tutorial, make sure the AEA.json file is set to Copy to Output folder.

Start Mosquitto:

C:\Program Files\mosquitto> mosquitto

Create a Data Message to send to the Receiver. To do this, copy the following into a file, ex. %TEMP%/message.json.

{
  "header": {
    "SrcModule": "Sender",
    "MessageType": "IODataReport",
    "ConfigVersion": 1,
    "MessageID": 252711801,
    "TimeStamp": 1666266380177
  },
  "device": [
    {
      "id": "18",
      "tags": {
        "HKLD_m": {
          "value": 10,
          "timestamp": 1666266380177,
          "quality_code": 0
        }
      }
    }
  ]
}

Start the Receiver application.

Publish the message.json to the Mosquitto Broker.

C:\Program Files\mosquitto> mosquitto_pub -t DataIn -f %temp%\message.json

Bus Client Tutorial

Namespace: Agora.Edge

The BusClient is an integral part of Edge SDK 2.0 and is implemented as a Singleton. The purpose of the BusClient is to send and receive messages with the edge message broker.

Bus Client

IMPORTANT: The routes.json provides the routes (a.k.a. topic mapping) so that messages can be routed between applications instead of publishing to all applications simultaneously. There is no facility to publish/subscribe to a global topic.

Configuration Parameters:

  • Name (string) - Used to set the MQTT Client Name that identifies the client to the Broker. This is used for Routing purposes. Default: Entry Assembly Name. Although "optional", it is recommended to specify the "Name" setting in the AEA.json configuration file.
  • AEA2:BusClient:Server (string): Server name hosting the MQTT Broker. Default: localhost. Commonly, this is set to mqtt-net-server when deployed in a container.
  • AEA2:BusClient:Port (uint): Server Port for MQTT broker hosting the MQTT Broker. Default: 707.
  • AEA2:BusClient:DeviceId (int): Default device id used when sending data from the application.
  • AEA2:BusClient:Subscriptions (string array): Array of string topics to subscribe. Routing is required to receive messages. Default: <empty>. Commonly, 'DataIn', and 'RequestIn' are frequently used.

Example of AEA.json configuration file:

{
    "Name": "MyApp",
    "AEA2": {
        "BusClient": {
            "Server": "localhost",
            "Port": 707,
            "DeviceId": 321,
            "Subscriptions": ["DataIn", "RequestIn", "CustomTopic"]
        }
    }
}

Properties:

  • Instance: BusClient - Used to access the Singleton instance and is the same as using the Agora.SDK.BusClient.
  • IsConnected: bool - True if connected.
  • Messages: BusMessageQueues - Provides access to the queues used to store incoming messages.

Methods:

  • NET
  • Python
  • void Connect(int timeout_msec) - Connects to the Bus using configuration settings.
  • bool IsConnected() - Returns true if BusClient is connected.
  • void SendData(IoDataReportMsg data, string msgTopic = "DataOut") - Sends an IoDataReportMsg to the Bus. Messages are sent to the topic, msgTopic which defaults to 'DataOut'.
  • void SendRequest(RequestMsg request, string msgTopic = "RequestOut") - Sends a RequestMsg to the Bus. Messages are sent to the topic, msgTopic which defaults to 'RequestOut'.
  • void SendMessage(string topic, MessageHeader header, string jsonMessage) - Sends a json message as json "payload" element and header as json "header" element using the specified topic.
  • async Task<bool> SendRawMessage(string topic, string message) - Sends a raw message as a string to the specified topic.
  • void SendEvent(EventMsg eventmsg, string msgTopic = "EventOut") - Sends a EventMsg to the Bus. Messages are sent to the topic, msgTopic which defaults to 'EventOut'.
  • connect(int timeout_sec) - Connects to the Bus using configuration settings.
  • is_connected() - Returns true if BusClient is connected.
  • send_data(data: IoDataReportMsg, msgTopic = "DataOut": str) - Sends an IoDataReportMsg to the Bus. Messages are sent to the topic, msgTopic which defaults to 'DataOut'.
  • send_request(request: RequestMsg, msgTopic = "RequestOut": str) - Sends a RequestMsg to the Bus. Messages are sent to the topic, msgTopic which defaults to 'RequestOut'.
  • void send_message(topic: str, header: MessageHeader, jsonMessage: str) - Sends a json message as json "payload" element and header as json "header" element using the specified topic.
  • void send_raw_message(topic: str, message: str) - Sends a raw message as a string to the specified topic.
  • send_event(event: EventMsg, msgTopic = "EventOut": str) - Sends a EventMsg to the Bus. Messages are sent to the topic, msgTopic which defaults to 'EventOut'.

Redis Client Tutorial

This tutorial provides information about key-value NoSQL type technology.

Redis is a NoSQL key-value cache that stores information in a hash table format. It provides the possibilities to store different type of structured data like strings, hashes, lists, sets, sorted sets, bitmaps and hyperloglogs.

With Edge SK 2.0 RedisClient functionality, developers can easily use Redis Server deployed on the gateway as a part of the Hermes Core Release.

Namespace: Agora.Edge

The RedisClient is implemented as a singleton. This singleton exposes a full-fledged RedisClient to interact with the server to store, retrieve values from Radis Server etc.

RedisClient can be configured from within the AEA.json as shown below.

Configuration Parameters:

  • Name (string) - Used to set the RedisClient Name that identifies the client to the server. Default: Entry Assembly Name. Although "optional", it is recommended to specify the "Name" setting in the AEA.json configuration file.
  • AEA2:RedisClient:Server (string): Server name hosting the Redis Server. Default: localhost. Commonly, this is set to the name of the container running Redis Server when deployed in a container.
  • AEA2:RedisClient:Port (uint): Server Port hosting the Redis Server. Default: 6379. Commonly, this is set as a container create option with the port exposing Redis Server when deployed as a container.

Example of AEA.json configuration file:

{
    "Name": "Sender",
    "AEA2": {
       "LogLevel": "Trace", 
        "RedisClient": {
            "Server": "localhost",
            "Port": 6379,
        }
    }
}

Properties:

  • Instance: Redis - Used to access the singleton instance and is the same as using the Agora.SDK.RedisClient.

  • Client - Used to access the Redis Server.

  • IsConnected: bool - True if connected to Redis Server.

Methods:

  • NET
  • Python
  • void Connect(int timeout_msec) - Connects to the Redis Server using configuration settings. If the server is not available, it will wait for timeout_msec and then it will try to connect in background. This is possible due to underneath Nuget Package StackExchange.Redis.

  • public void Dispose() - Cleans up resources required to keep the connection live to the Redis Server. Dispose makes underneath connection closed.

  • def connect(self, sec: float): - Connects to the Redis Server using configuration settings. If the server is not available, it will wait for timeout_msec and then it will try to connect in background. This is possible due to underneath Py Package Redis-Server.

  • def dispose(self): - Cleans up resources required to keep the connection live to the Redis Server. Dispose makes underneath connection closed.

For Local Development and Testing

To get started with Redis Stack using docker, first you need to select a docker image:

  • redis/redis-stack - contains both Redis Stack server and RedisInsight. This container is best for local development because yu can use the embedded RedisInsight to visualize your data.

To start a Redis Stack container using the Redis Stack image, run the following command in your terminal:

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

Add an AEA.jsonfile to project with below setting:

{
    "Name": "Sender",
    "AEA2": {
       "LogLevel": "Trace", 
        "RedisClient": {
            "Server": "localhost",
            "Port": 6379,
        }
    }
}
  • NET
  • Python

Create a Simple Console .NET Application

Programe.cs

using System.Text.Json;
using static Agora.SDK;

internal class Program
{
   private static void Main(string[] args)
    {
        "Starting".LogHeading();
        Redis.Connect(2000);
        if (Redis.IsConnected)
        {
            var redisValues = Redis.Client?.ListRange("Key");
        }
    }
}

Create a Simple Console Application in Python

from agoraiot import *

redisclient = redisClientSingleton.connect(2)

On running the app locally, Application should connect and print the below output.

RedisClient

Getting Started with the Agora Edge App .NET SDK

The .NET SDK provides functionality for developing .NET edge applications using .NET6.0 and above.

The .NET SDK is distributed on Nuget as AgoraIoT.Edge.App.SDK.

Several methods for getting started are provided using several environments, including:

  • Command Line
  • Visual Studio (Community, Pro, etc.)
  • Visual Studio Code

Create a Console App

Pick the method you would like to create a new Console App.

Creating an Agora Edge App using Visual Studio 2022 and .NET Core 6

Create Console App:

  1. Start Visual Studio.
  2. Create a new project targeting a Console App (File > New Project...).
  3. Enter a Project Name, Location, and Solution Name.
  4. For the framework select .NET Core 6 (Long-term support).

Add 'AgoraIoT.Edge.App.SDK' NuGet Package:

  1. On the Solution Explorer, right click on the solution and select Manage Nuget Packages for Solution to bring up the NuGet Package Manager.
  2. Click the check box Include prerelease and make sure the Package Source is set to Nuget.
  3. Search for AgoraIoT.
  4. Select AgoraIoT.Edge.App.SDK in the list of packages.
  5. Agree to the License Agreement.

Creating an Agora Edge App using Visual Studio Code and .NET Core 6

Create Console App:

Follow the Microsoft tutorial to create a Console app, EXCEPT in Step 6 run the following commands:

> dotnet new console -n ProjectName -f net6.0 --use-program-main
> cd ProjectName
> dotnet add ProjectName.csproj package AgoraIoT.Edge.App.SDK --version 1.0.11-beta

Creating an Agora Edge App using Command Line and .NET Core 6

Create Console App:

From folder where you want to create a project within, enter

> dotnet new console -n ProjectName -f net6.0 --use-program-main
> cd ProjectName
> dotnet add ProjectName.csproj package AgoraIoT.Edge.App.SDK --version 1.0.11-beta
"Hello World!"

Replace the code in Program.cs with the following code and run the application:

"Hello World!".LogInfo();

Creating an Edge Application Container Image

Using the Dockerfile.sample provided within the application, create a Dockerfile for your project.

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/runtime-deps:6.0-alpine AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build

WORKDIR /src
COPY src/. /src

RUN dotnet restore "ConsoleApp/ConsoleApp.csproj" -r linux-musl-x64
RUN dotnet build "ConsoleApp/ConsoleApp.csproj" -c Release -o /app/build --no-restore -r linux-musl-x64

FROM build AS publish
RUN dotnet publish "ConsoleApp/ConsoleApp.csproj" -p:PublishSingleFile=true -r linux-musl-x64 --self-contained true -p:PublishTrimmed=False -p:TrimMode=Link -c Release -o /app/publish --no-restore

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["./ConsoleApp"]

Create your docker container by running the following command within the project folder with the Dockerfile:

> docker build .

Getting Started with the Agora Edge App Python SDK

The Python SDK provides modules for developing an Agora Edge App using Python 3.8 or higher. All examples assume this has been installed.

You should also update 'pip':

> pip install -U pip

Create an Edge App

The Agora Edge App Python SDK provides the following modules combined within one Python package called agoraiot:

  • agora_logging
  • agora_busclient
  • agora_config
  • agora_utils

To install the package use:

> pip install agoraiot
"Hello World!"

Create a helloWorld.py file:

from agoraiot import logger

logger.info("Hello World!")

Execute using Python:

> python helloWorld.py

Creating an Edge Application Container Image

Create the following files:

helloworld.py [from above]
Dockerfile

Dockerfile:

FROM alpine:3.17
USER root
ENV PATH="${PATH}:/sbin"
RUN apk add bash gcompat libc6-compat icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib python3 py3-pip
RUN pip install agoraiot
ADD ./src src
CMD python3 src/helloWorld.py

Run the following command:

> docker build .

Twin Property Tutorial

This tutorial provides information on how to use the TwinProperty module of SDK to subscribe to changes in Desired properties and write the Reported properties. It uses Redis as the data store. The properties must be associated to a Group Name.

This module allows more than 1 Edge application to access the same Group Name and Desired property with Redis acting as the shared repository for information.

Namespace: Agora.Edge

A singleton member of Agora.SDK called Twin is used by applications to interact with twin properties.

Configuration

The module uses Redis as the data store, hence, has dependency on the SDK's Redis module. The Edge application must have the following configuration in its primary configuration file

{
    "AEA2":{
        "RedisClient": {
            "Server": "localhost",
            "Port": 6379,
        }
    }
}

Additionally, the application should connect to Redis using Redis.Connect() method.

  • NET
  • Python

Properties:

  • Instance: TwinProperty - Used to access the singleton instance and is the same as using the Agora.SDK.TwinProperty.

Event

  • ChangeEvent - Used to subscribe/unsubscribe to a Desired property or a Group Name

Methods:

  • public bool SetReportedProperty(string propName, string propValue, string tpId) - Method to set reported property of name propName associated to group name tpId with value propValue. The method uses redis key pattern as mentioned below. :-
twin_properties/{tpId}/reported/{propName}
twin_properties/{tpId}/reported/__appname
  • public string GetDesiredProperty(string propName, string tpId) - Method to read the value of a desired property i.e. propName associated to a GroupName tpId. The method uses the key pattern as mentioned below.:-
twin_properties/{tpId}/desired/{propName}
  • public string GetReportedProperty(string propName, string tpId) - Method to read the value of a reported property propName assoicated to group name tpId. The method uses key pattern as mentioned below:-
twin_properties/{tpId}/reported/{propName}
  • def observe(app_callback:Any, tp_group_id:str, property_name:str=None) - Method to add observable for tp_group_id and property_name. If property_name set to None, observable is set for Redis Key pattern:
twin_properties/{tp_group_id}/desired/*
  • def stop_observe(tp_group_id:str,property_name:str=None) - Method to remove observable for tp_group_id and property_name if it exists. If property_name set to None, observable is removed for the pattern:
twin_properties/{tp_group_id}/desired/*
  • def set_reported_property(prop_name:str, prop_value:any, tp_group_id:str): - Method to set reported property. The method uses redis key as mentioned below. :-
twin_properties/{tp_group_id}/reported/
twin_properties/{tp_group_id}/reported/__appname
  • def get_desired_property(prop_name:str, tp_group_id:str): - Method to read the value of a desired property i.e. prop_name. The method uses the key pattern as mentioned below. :-
twin_properties/{tp_group_id}/desired/
  • def get_reported_property(prop_name:str, tp_group_id): - Method to read the value of a reported property prop_name. The method uses key pattern as mentioned below:-
twin_properties/{tp_group_id}/reported/

For Local Development and Testing

  • NET
  • Python

Create a Simple Console .NET Application

using System.Text.Json;
using static Agora.SDK;

namespace redis_sdk_project
{
    public class Program
    {
        public static void appCallback(string groupId, Dictionary<string, string> data)
        {
            foreach (var kvp in data)
            {
                $"AppCallback called for group {groupId} and the Key: {kvp.Key}, and Value: {kvp.Value}\n".LogInfo();
            }
        }

        public static void Main(string[] args)
        {
            Redis.Connect(30);
            var propName = "TwinPropName";
            var tpGroupId = "Group2";
            //Add subscription to all desired properties associated to a GroupName
            Twin["Group1"].ChangeEvent += appCallback;        
            //Add subscription to a particular desired property associated to a GroupName
            Twin["Group2"][propName].ChangeEvent += appCallback2;            
            //Set reported Property
            Twin.SetReportedProperty(propName, "30", tpGroupId);
            //Get desired property value
            $"Desired Property:{propName}- {Twin.GetDesiredProperty(propName, tpGroupId)}".LogInfo();
            //Get reported property
            $"Reported Property:{propName}- {Twin.GetReportedProperty(propName, tpGroupId)}".LogInfo();
            //Remove subscription
            Twin["Group2"][propName].ChangeEvent -= appCallback2;
            Console.ReadLine();
        }
    }
}

Create a Simple Console Application in Python

from agora_twin_property import Twin
from agora_logging import * 


def callback(twin:any):
    logger.info(f"callback {twin.tp_id}: {twin.key}: {twin.value}")

def callback1(twin:any):
    logger.info(f"callback 1 {twin.tp_id}: {twin.key} :{twin.value}")
    
#Subscribe to all desired properties under a GroupName
Twin.observe(callback, "Group1")
#Subscribe to a desired property under a GroupName
Twin.observe(callback1,"Group2","TwinPropName")

# Set reported property 
logger.info(f"Set property {Twin.set_reported_property("TwinPropName",30,"Group1")}")
# Get reported property
logger.info(f"Reported property: TwinPropName- {Twin.get_reported_property("TwinPropName","Group1")}")
# Get desired property
logger.info(f"Desired Property: TwinPropName- {Twin.get_desired_property("TwinPropName","Group1")}")
#Stop Observable
Twin.stop_observe("Group1")

On running the application locally, application should connect and print the below output.

TwinProperty

  • Edit this page
In this article
Back to top Copyright SLB, Published Work. All rights reserved.