Show / Hide Table of Contents

Reference Manual

This is a reference manual for Agora Edge Apps (AEA) .NET SDK and Agora Edge Apps (AEA) Python SDK. It provides a detailed information about Logging, Configuration, Messageing, Utilities and containers. This reference manual is an essential resource for developers who want to build high-quality applications with AEA .NET and AEA Python. It provides detailed documentation for all the tools and features available, as well as code samples and tutorials to help developers get started quickly.

The AEA .NET SDK provides functionality for developing Agora Edge applications using .NET6.0 and above.

The AEA Python SDK provides functionality for developing Agora Edge applications using Python 3.8.

  • NET
  • Python

AEA .NET SDK

You can download the AEA .NET SDK from Nuget as AgoraIoT.Edge.App.SDK.

AEA Python SDK

You can download the AEA Python SDK from agoraiot.

Logging

Logging is an essential aspect of software development, as it allows developers to track and debug issues that arise during the applications's runtime. Both .NET and Python SDKs provide logging capabilities that enable developers to analyse application events and errors.

  • NET
  • Python

To use the SDK, include the Nuget package and add the following statement to the C# source.

using static Agora.SDK;

String Extensions for Logging

The SDK provides a set of extensions to System.String which allows strings to be written directly to the log and to the console.

The available string extensions include:

"Message".LogTrace();
"Message".LogDebug();
"Message".LogInfo();
"Message".LogWarn();
"Message".LogError();
"Message".LogFatal();
"Message".LogHeading();
"Message".LogException(ex);
"Message".Cout(); /* Writes to `Console.Write("Message")` */

All LogLevels except Info, Trace, and Warning will record source code location to ease troubleshooting.

Example

"Hello World!".LogInfo();
Tip

Each extension returns a string which allow the ability to log string modifications or to continue to use the string within another call.

"Hello World!".LogInfo().ToLower().LogInfo();

Console.WriteLine("Hello World!".LogInfo());

produces:

I(99) - Hello World!
I(99) - hello world!
I(99) - Hello World!
Hello World!

Each message begins with the first letter of the LogLevel, such as T - Trace, D - Debug, I - Info and so on, followed by the number of milliseconds since the application started, in parentheses.

The SDK provides a static class AgoraLogger that can be used to provide an additional ILoggingTarget and to programmatically set the logging level.

interface ILogger

  • GetLevel()/SetLevel(LogLevel level)

    Allows the programmatic setting of the logging level, overriding the default set in the AEA.json file.

  • void Write(LogLevel level, string Message, [CallerMemberName] string memberName_DoNotUse = "", [CallerFilePath] string sourceFilePath_DoNotUse = "", [CallerLineNumber] int sourceLineNumber_DoNotUse = 0)

    Writes a message to the log. The memberName, sourceFilePath, and sourceLineNumber are not used if the LogLevel is Info, Warn, or Fatal.

  • void WriteHeading(string Message)

    Writes a heading message to the log at LogLevel.Info. This method helps to find sections in the LogTarget by placing the Heading on the right side of the text.

  • void WriteException(LogLevel level, Exception ex, string Message, [CallerMemberName] string memberName_DoNotUse = "", [CallerFilePath] string sourceFilePath_DoNotUse = "", [CallerLineNumber] int sourceLineNumber_DoNotUse = 0)

    Writes an exception message to the log and all InnerExceptions. The memberName, sourceFilePath, and sourceLineNumber are not used if the LogLevel is Info, Warn, or Fatal.

A singleton logger object of type AgoraLogger is provided by the SDK and is imported as shown:

from agoraiot import logger

Logging can be done either by invoking the correct method of logger directly with desired log level, or using methods for each of the logging levels as shown below.

from agoraiot import logger, LogLevel
logger.write(LogLevel.TRACE, "trace message")

or

logger.info("info message")
logger.trace("trace message")
logger.debug("debug message")
logger.warn("warning message")
logger.error("error message")
logger.fatal("fatal message")
logger.heading("heading message")
ex=Exception("An Exception of Some Sort")
logger.exception(ex, "exception message")

All LogLevels except Info, Trace, and Warning will record source code location to ease troubleshooting.

The logger also supports the following methods:

  • set_level(LogLevel level)

    Allows the programmatic setting of the logging level, overriding the default set in the AEA.config.json file.

  • write(LogLevel level, str Message)

    Writes a message to the log. The memberName, sourceFilePath, and sourceLineNumber are not used if the LogLevel is Info, Warn, or Fatal.

  • heading(str Message)

    Writes a heading message to the log at LogLevel.Info. This method helps to find sections in the LogTarget by placing the Heading on the right side of the text.

  • exception(Exception exception, str Message, LogLevel level=LogLevel.WARN)

    Writes an exception message to the log and all InnerExceptions.

Configurable Logging Settings

  • AEA2:LogLevel (string) - [optional] One of ("Trace", "Debug", "Info" (default), "Off"). This setting is not case sensitive and provides the capability to specify the minimum level of log statements included in the log. This setting affects all Logging Targets contained by the Logger.

Example:

AEA.json for Logging:

{
  "Name": "DemoApp",
  "AEA2": {
    "LogLevel": "Debug"
    }
  }
}

Configuration

Configuration is the process of specifying settings and parameters that control the behavior of an application. The sections below show how to access Configuration key/value and to monitor for changes in the configuration at runtime. The SDK constructs the configuration using several sources, as described in the Configuration Sources section.

Accessing Configuration Settings

Configuration setting values are accessed using configuration singleton. Settings are accessed hierarchically by separating the levels of the hierarchy with colons.

Examples:

All examples use the following configuration:

AEA.json:

{
    "App" : {
        "SomeSetting": true,
        "Fruit": [ "Apple", "Banana", "Pear" ],
        "FruitObjects": [ {"Name": "Apple"}, {"Name": "Banana"}, {"Name": "Pear"}]
    }
}

Retrieving a setting

  • NET
  • Python
using static Agora.SDK;
...
Console.Writeline(Config["App:SomeSetting"]);
from agoraiot import config
...
print( config["App:SomeSetting"] )

Retrieving an array

  • NET
  • Python
using static Agora.SDK;
using Microsoft.Extensions.Configuration;
...
var fruits = Config.GetSection("App:Fruit").Get<List<string>>();

foreach(var fruit in fruits)
    Console.WriteLine(fruit);
from agoraiot import config

fruits = config["App:Fruit"]

for fruit in fruits:
    print( fruit )

Output:

Apple
Banana
Pear

Accessing array of Objects

  • NET
  • Python
using static Agora.SDK;
using Microsoft.Extensions.Configuration;

class Fruit { public string Name { get; set; } }

List<Fruit> fruits = Config.GetSection("App:FruitObjects").Get<List<Fruit>>();

foreach(var fruit in fruits)
    Console.WriteLine(fruit.Name);
from agoraiot import config

class Fruit:
    def __init__(self, name):
        self.name = name

fruits = config["App:FruitObjects"]

for fruitdata in fruits
    fruit = Fruit(fruitdata["Name"])

# to deserialize directly you will need to create custom json encoders/decoders

Setting Default Configuration Settings and Overrides

The configuration is built from a set of sources that starts with Defaults and ends with Overrides as shown in Configuration Overview. To set defaults or overrides use the Defaults or Overrides dictionaries.

Example:

  • NET
  • Python
using static Agora.SDK;

...
Config.Defaults["App:SettingName"] = "Default value if none provided";
Config.Overrides["App:OtherSettingName"] = "Overrides all other sources";
Config.Build();

from agoraiot import config

...
config.defaults["App:SettingName"] = "Default value if none provided";
config.overrides["App:OtherSettingName"] = "Overrides all other sources";
config.build()

Runtime Configuration Changes

Because the alternate configuration and any Key-Per-File settings can be modified while the application is running, an application should monitor if the Configuration has changed.

Monitoring for Configuration Changes

  • NET
  • Python
    Config.Changed += ConfigChanged;
...
    private void ConfigChanged(object? sender, EventArgs e)
    {
        "Configuration Changed".LogInfo();
    }
def config_change_handler():
    logger.info("Configuration Changed")

config.observe_config( config_change_handler )

Monitoring for Configuration Changes of Individual Settings

  • NET
  • Python
var mySetting = Agora.ObservableSetting.Get("AEA2:LogLevel");
mySetting.PropertyChange += SettingChange;

...
private void SettingChange( object? sender, 
                            System.ComponentModel.PropertyChangedEventArgs e)
{
    if (sender is Agora.ObservableSetting o)
        $"Setting Changed to `{o.Value}'".LogInfo();
}
def setting_changed(val):
    logger.info(f"Setting Changed to {val}")

config.observe("AEA2:LogLevel", setting_changed )

Configuration to JSON

  • NET
  • Python
using Microsoft.Extensions.Configuration;

var json = Config.SerializeToJson();
if ( json != null )
    json.ToJsonString(new JsonSerializerOptions() { WriteIndented = true }).LogInfo();
from agoraiot import config, logger
import json

logger.info( json.dumps(config, indent=4) )

Messaging

Messaging is an important aspect of software development as it enables applications to communicate with each other and exchange data. The SDKs provide messaging capabilities that enable developers to build robust and scalable distributed systems.

BusClient

The BusClient is used for sending and receiving messages from an MQTT broker.

  • NET
  • Python

Properties:

  • IsConnected: bool - True if connected.
  • Messages: BusMessageQueues - Provides access to the queues used to store incoming messages. See BusMessageQueues for more information.

Methods:

  • public void Connect()
  • public void Disconnect()
  • public void SendMessage(string topic, string payload)- Unlike a general MQTT Message Bus Client, methods sent to the Broker will be prepended with /{ModuleName}/ when the messages arrive so that the routing can occur.

Properties:

  • is_connected: bool - True if connected.
  • messages: BusMessageQueues - Provides access to the queues used to store incoming messages. See MessageQueues for more information.

Methods:

  • configure() - Reads settings from AEA configuration.
  • connect(sec) - Connects... waiting up to sec seconds. If not connected, connection will continue to try asynchronously.
  • disconnect() - Disconnects.
  • send_data(msg: IoDataReportMsg, topic = "DataOut")- Sends IoDataReportMsg (msg) to topic (default is "DataOut").
  • send_message(topic, header, payload)- Combines header and payload into json msg and sends message to topic.
  • send_raw_message(topic, payload)- Sends message payload to topic.
  • send_request(msg: RequestMsg, topic = "RequestOut")- Sends RequestMsg (msg) to topic (default is "RequestOut").

Configuring BusClient

  • Name: Used to identify the client to the broker.
  • AEA2:BusClient:
    • Mock (default = false): Used for testing by looping back sent messages to the incoming message queues. Messages are not sent broker.
    • Server (default = 'localhost'): Server or container name where MQTT Broker is running.
    • Port (default = '707'): Port number of MQTT Broker.
    • DeviceId (default = '999'): The default Device Id to use when sending data.
    • Subscriptions: The array of incoming topics to subscribe to. "DataIn" and "RequestIn" are required to receive IoDataReportMsg and RequestMsg, respectively.

Example

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

BusClient and the Agora Edge Apps Message Broker

The BusClient can be accessed using the SDK and is used for interacting with the Agora Edge Apps Message Broker.

The BusClient's purpose is to send and receive messages with the Broker. Unlike a traditional MQTT Broker, the Agora Edge Apps Message Broker is responsible for routing the messages between modules. For example, Module1 may produce DataOut messages which the Broker can be configured to route to Module2/DataIn.

IoDataReportMsg - DataIn/DataOut

The IoDataReportMsg helps to create data messages for the DataIn/Out messages and is accessed using the BusClient message queues.

The class diagram for an IoDataReportMsg is shown below.

IoDataReportMsg Class Diagram

The class diagram is complex as it takes advantage of many generic classes, however it will ultimately represents the JSON messages shown below, encapsulating data from potentially multiple device and multiple tags single values per tag.

The following example shows how to mock the bus client, send data, and parse the incoming data messages:

AEA.json:

{
    "AEA2": {
        "BusClient": {
        "Mock": true,
        "DeviceId": 300,
        }
    }
}
  • NET
  • Python
double [] temperatures = {71.0, 72.0, 73.0, 72.5, 53.8, 68.3, 79.3, 85.4};

IoDataReportMsg msg = new();
int i = 0;
foreach(var t in temperatures)
{
    msg.Add($"TEMP{i}", new IoPoint() {value = t, quality_code = 0 };
    i++;
}
Bus.SendData(msg);

foreach(var m in Bus.Message.GetDataMessages())
    foreach(var d in m.device)
    {
        $"DeviceId = {d.Id}".Cout();
        "Tags:".Cout();
        foreach(var t in d.Tags)
            $"--- {t.Key} - {t.Value.value}".Cout();
    }
from agoraiot import *

temperatures = [71.0, 72.0, 73.0, 72.5, 53.8, 68.3, 79.3, 85.4]

msg = IoDataReportMsg()
i = 0
for t in temperatures:
    msg.add_data(f"TEMP{i}", IoPoint(value = t, quality_code=0))
    i = i + 1

bus_client.send_data(msg)

for m in bus_client.messages.get_data_messages():
    for d in m.device:
        print(f"DeviceId = {d.id}")
        print("Tags:")
        for key,value in d.tags.items():
            print(f"--- {key} - {value.value}");
Requests

The SDK provides the ability to construct and receive Requests. A request is a simple message, which allows a Command name and a set of named Arguments or Device data to be encapsulated within. The following is an example of creating a request:

  • NET
  • Python
var req = new RequestMsg();
req.Command = "RequestName";
req.Payload.Add( "Parameter1", "Value1" );
req.Payload.Add( "Parameter2", "Value2" );

int correlationId = Bus.SendRequest(req);
req = RequestMsg(requestName = "RequestName")
req.payload["Parameter1"] = "Value1"
req.payload["Parameter2"] = "Value2"

correlation_id = bus_client.send_request(req)

Receiving requests are also simple and is shown below with the creation of a response message that uses the requests correlation id (RequestMsg.Id):

  • NET
  • Python
if (Bus.Messages.HasRequestMessages)
{
    var requests = Bus.Messages.GetRequestMessages();

    foreach (var request in requests)
    {
        if (request.Command == "RequestName")
        {
            // ... do something interesting ...
        }
    }
}
if bus_client.messages.has_request_messages:
    msgs = bus_client.messages.get_request_messages()
    for msg in msgs:
        if msg.header.MessageType == "RequestName":
            # do something interesting
Events

Eevents Schema should be used in context of messages that needs to be sent as an Alert or Events to Nimbus via Passthrough handler.

The class diagram for an EventMsg Schema is shown below.

EventMgSchema Class Diagram{ .img-thumbnail }


BusMessageQueues

The BusMessageQueues is used for accessing the messages arriving via the BusClient. To use the Agora Edge Apps MQTT Broker, you should use BusClient as it provides methods that enable the core messages used by Agora Edge Apps.

  • NET
  • Python

Properties:

  • HasApplicationMessage: bool - True if Application Messages are available.
  • HasDataInMessages: bool - True if DataIn Messages are available. Requires AEA2:BusClient:UseDataIn = True in the application configuration.
  • HasRequestInMessages: bool - True if RequestIn Messages are available. Requires AEA2:BusClient:UseRequestIn = True in the application configuration.
  • HasEventMessages: bool - True if Event Messages are available.

Methods:

  • GetApplicationInMessages(): IList<byte[]> - Returns a list of raw Application Messages.
  • GetDataInMessages(): IList<IoDataReportMsg> - Returns a list of DataIn Messages waiting in the queue converted to IoDataReportMsg.
  • GetRequestInMessages(): IList<RequestMsg> - Returns a list of Request Messages waiting in the queue converted to RequestMsg.
  • GetEventMessages(): IList<EventMsg> - Returns a list of Event Messages waiting in the queue converted to EventMsg.

Properties:

  • has_application_messages: bool - True if Application Messages are available.
  • has_data_messages: bool - True if DataIn Messages are available.
  • has_config_messages: bool - True if ConfigIn Messages are available.
  • has_request_messages: bool - True if RequestIn Messages are available.
  • has_event_messages: bool - True if event Messages are available.

Methods:

  • get_application_messages(): - Returns a list of raw Application Messages.
  • get_data_messages(): - Returns a list of DataIn Messages waiting in the queue converted to IoDataReportMsg.
  • get_config_messages(): - Returns a list of raw ConfigIn Messages.
  • get_request_messages(): - Returns a list of raw StateRequest Messages.
  • get_event_messages(): - Returns a list of raw event Messages.

Utilities

Both .NET and Python SDKs provide a powerful utility library and tools that enable developers to simplify their development workflows and perform common tasks more efficiently.

Timestamps

AgoraTimeStamp and UTCDateTime are the two methods for consistent handling of timestamps. The AgoraTimeStamp is defined as the number of milliseconds since Jan. 1, 1970 (UTC) as a double . It is expected that all times used between applications use AgoraTimeStamps to avoid miscommunication.

Note

AgoraTimeStamp is not affected by timezone. When passing in a datetime object to AgoraTimeStamp it converts the time correctly to UTC time and delivers the correct corresponding timestamp. Additionally, the datetime returned by UTCDateTime is set to the UTC time zone.

For clarity, the for both AgoraTimeStamp and UTCDateTime are provided below:

  • NET
  • Python
public static class Time
{
    public static double AgoraTimeStamp(DateTime? tm = null)
    {
        if (tm == null)
            return (DateTime.UtcNow - DateTimeOffset.UnixEpoch).TotalMilliseconds;
        return (tm.Value.ToUniversalTime() - DateTimeOffset.UnixEpoch).TotalMilliseconds;
    }

    public static DateTime UTCDateTime(double tm) => 
        new(DateTimeOffset.UnixEpoch.Ticks + (long)(tm * 10000), DateTimeKind.Utc);
}
from datetime import datetime, timezone

def AgoraTimeStamp(tm=datetime.utcnow()) -> float:
    dt_utc = datetime(tm.year, tm.month, tm.day,
                      tm.hour, tm.minute, tm.second, tm. microsecond,
                      tzinfo=timezone.utc)
    delta = dt_utc.timestamp() - tm.timestamp()
    return (tm.timestamp() + delta) * 1000

def UTCDateTime(tm: float) -> datetime:
    dt = datetime.utcfromtimestamp(tm/1000).replace(tzinfo=timezone.utc)
    return dt

now_time_stamp = AgoraTimeStamp()
then_time_stamp = AgoraTimeStamp() - 10000

now_date_time = UTCDateTime(now_time_stamp)
then_date_time = UTCDateTime(then_time_stamp)

Misc. .NET Utilities

Agora.Utilities.Subject / Observable<T> / ObservableString

Subject, Observable<T>, and ObservableString are used to observe changes in variables across an application.

Observable<T> and ObservableString both derive from Subject which implement INotifyPropertyChange.

Subjects allow one to Invoke an OnChanged event without changing some underlying value.

Observable<T> and ObservableString will Invoke an OnChange event anytime the value they contain changes.

One must retrieve a Subject by name.

Examples

var mySubject = Subject.Get("MySubject");
var counter = Observable<int>.Get("Counter");
var realTimeValue = Observable<double>.Get("BPOS");

Since Subject inherits from INotifyPropertyChange one can subscribe to changes using the PropertyChanged event handler.

Examples

realTimeValue.PropertyChange += OnBPOSChange;

If the Observable value changes,

realTimeValue.Value = 123;

OnBPOSChange will be called.

Since Subjects do not have Values, a Subject event is invoked by just calling:

mySubject.OnChanged();

Containers

Both .NET and Python have a standard container technology called Docker. Docker provides a platform for developers to package their applications and all their dependencies into a single container, making it easy to deploy and run their applications consistently across different environments.

  • NET
  • Python

Building Docker Containers for .NET Applications

When you add the Nuget package to an application, it adds a file called Dockerfile.sample in the NugetContent folder.

NugetContent/Dockerfile.sample

This sample provides a starting point to construct a Docker container image.

The Dockerfile uses alpine as the base image and then creates a build image on top of it. The build image is used to restore/build the project.

Building Docker Containers for Python Applications

This sample provides a starting point to construct a Docker container image for Python, Refer to Container Overview For Python App.

Redis Client

Redis Client 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 available on the gateway as a part of the Hermes Core Release.

Using Redis Client

Namespace: Agora.Edge

The RedisClient is implemented as a singleton. The purpose of the RedisClient is to store, retrieve values from Radis Server.

Example of AEA.json configuration file:

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

Connect to the Redis Server

Use the following command to connect to the Redis Server.

  • NET
  • Python
using System.Text.Json;
using static Agora.SDK;

internal class Program
{
   private static void Main(string[] args)
    {
        "Starting".LogHeading();
        Redis.Connect(2000);
    }
}
from agoraiot import *

redisclient = redisClientSingleton.connect(2)

Storing and Retrieving Value using Redis Client

Use the following command to store and retrieve value.

  • NET
  • Python
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)
        {
            //This one is used to store value
            Redis.Client.StringSet("key", "value");
            //This one is used to Get value
            var value = Redis.Client.StringGet("key");
            $"Value {value}".LogInfo();
        }
    }
}
redisclient = redisClientSingleton.connect(2)
if redisclient.ping():
    redisclient.set('my_key', 'my_value')
    result = redisclient.get('my_key')
    print(result)

Sample App

The following shows a sample that can be used as a starting point when creating a new edge app. It assumes that the SDK has been added to your application.

AEA.json:

{
    "Name": "SampleApp",
    "AEA2": {
        "LogLevel": "Info",
        "BusClient": {
            "Server": "localhost",
            "Port": 1883,
            "Subscriptions": ["DataIn", "RequestIn"]
        }
    }
    "AppSettings": {
        "Setting1": 1234
    }
}
  • NET
  • Python
using static Agora.SDK;

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

    public void Configure()
    {
    }

    public void Run()
    {
    }
}
redisclient = redisClientSingleton.connect(2)
if redisclient.ping():
    redisclient.set('my_key', 'my_value')
    result = redisclient.get('my_key')
    print(result)
  • Improve this Doc
In This Article
Back to top Copyright © 2023 SLB, Published Work. All rights reserved.