# Sending Telemetry Data from an Edge Device
This is the first of a series of tutorials which explain how to accomplish various functions within the context of an ABB Ability™ Edge. On the ABB Ability™ Edge runtime, the proxy and broker modules provided by the platform are responsible for handling any communication to or from the ABB Ability™ Edge environment. Thus, the any code written within custom modules has built-in security and can be relatively simple.
This tutorial will demonstrate:
- How to create a simple custom ABB Ability™ Edge module
- How to create a simulated device referenced by that module
- How to send telemetry data for that device
# Before You Start
The tutorial is written in C# using the .NET Core framework.
A basic console application will be created. Familiarity with .NET
C# is
recommended.
Once the application is compiled, Docker will be used to create and push an image to a remote container registry.
Once the image is deployed, the ABB Ability™ Edge runtime is responsible for
identifying custom modules to be loaded and then pulling down the appropriate
image and creating a container instance. The corresponding code for this
tutorial can be found at this ABB Codebits
repository
under the "01-send-telemetry" folder. The "SendTelemetry" project references
the ABB.Ability.IotEdge.CST.Modules.CSharp.Common
project, which can be found
under the "common" folder in the same repository.
For this tutorial, we are using an MIT-licensed nuget package called MQTTnet, but other MQTT (Message Queuing Telemetry Transport) wrappers can be used.
# Steps
Open the
ABB.Ability.IotEdge.CSTModules.CSharp.SendTelemetry
project in Visual Studio, VS Code, or the IDE of your choice. The point of focus in this tutorial is theSendTelemetryModule
, which is created on startup in theProgram.cs
class.SendTelemetryModule
inherits fromModuleBase
in theABB.Ability.IotEdge.CST.Modules.CSharp.Common
library. This base library is used to store functions that are common to all the tutorials, such as managing the MQTT client, retrieving configuration properties, and using reflection to call invokable methods used by remote commands in later tutorials.The ABB Ability™ Edge runtime is responsible for providing necessary information as environment variables to containers running custom modules. There is a class called
EdgeConfiguration
in theABB.Ability.IotEdge.CST.Modules.CSharp.Common
library which is responsible for retrieving those environment variables and providing them as properties to the code in these tutorials. Since the configuration is retrieved upon module startup and is immutable, all its properties are populated in the constructor:public EdgeConfiguration() { ClientId = Env.GetEnvironmentVariable("mqtt_client_id"); ServerUri = new Uri(Env.GetEnvironmentVariable("mqtt_url")); Username = Env.GetEnvironmentVariable("module_id"); Password = File.ReadAllText( Env.GetEnvironmentVariable("mqtt_password_file")); MethodsInTopic = Env.GetEnvironmentVariable("topics_methods_in"); MethodsOutTopic = Env.GetEnvironmentVariable("topics_methods_out"); MessagesOutTopic = Env.GetEnvironmentVariable("topics_messages_out"); ModelInTopic = Env.GetEnvironmentVariable("topics_model_in"); CleanSession = true; ObjectId = Env.GetEnvironmentVariable("object_id"); LocalInTopic = Env.GetEnvironmentVariable("topics_local_in"); LocalOutTopic = Env.GetEnvironmentVariable("topics_local_out"); }
In the
ModuleBase
class, theStartMessagingClient
method is responsible for creating the MQTTnet client used to subscribe and publish messages to the appropriate service bus topic.
First, an instance of theIManagedMqttClient
is created. Since_mqttClient
is a protected field in theBaseModule
class, this client instance is used by all tutorials:_mqttClient = new MqttFactory().CreateManagedMqttClient();
Next, the
ManagedMqttClientOptionsBuilder
class provided by MQTTnet is used to configure various options for the messaging client.
Again, all the configuration values are provided by the Edge runtime as environment variables, so any custom module is guaranteed to have the correct values once theEdgeConfiguration
class retrieves them from the environment variables:{ var options = new ManagedMqttClientOptionsBuilder() .WithClientOptions( new MQTTnet.Client.MqttClientOptionsBuilder() .WithClientId(this.Configuration.ClientId) .WithTcpServer( this.Configuration.ServerUri.Host, this.Configuration.ServerUri.Port) .WithCredentials( this.Configuration.Username, this.Configuration.Password) .WithKeepAlivePeriod( TimeSpan.FromMilliseconds( DEFAULT_KEEP_ALIVE_PERIOD_MS)) .WithCleanSession(this.Configuration.CleanSession) .Build()) .WithAutoReconnectDelay( TimeSpan.FromMilliseconds(DEFAULT_AUTO_RECONNECT_DELAY_MS)) .Build(); }
An event handler must also be defined for the
ApplicationMessageReceived
event. Any message that is delivered on a subscribed topic will be handled here, with theMqttApplicationMessageReceivedEventArgs
portion of the handler signature containing information about the message.The code below assigns the abstract
OnApplicationMessageReceived
method to handle these events – that method will be overridden in modules that inherit fromModuleBase
._mqttClient.ApplicationMessageReceived += OnApplicationMessageReceived;
Finally, the MQTT client itself must be started with the supplied options:
await _mqttClient.StartAsync(options);
Back in the
SendTelemetryModule
class, first look at theStartModuleAsync
method. TheManualResetEventSlim
object is used to keep the container in a running state until it is forcefully stopped.Next a subscription is set up on the MQTT client for the “model in” topic. This will ensure that any messages containing information about the models for this module or its references will be received properly.
Note that the topic ends with the pound (#) character; this acts a wildcard and refers to any topic that begins with the characters prior to the pound.
public override async Task StartModuleAsync(string[] args) { Console.WriteLine("Starting C# Send Telemetry Module..."); var terminate = new ManualResetEventSlim(); var topic = $"{ this.Configuration.ModelInTopic}/#"; await _mqttClient.SubscribeAsync(topic).ContinueWith( (e) => Console.WriteLine($"Subscribed to topic '{topic}'")); //wait indeterminately until the container/application is stopped terminate.Wait(); }
In the
OnApplicationMessageReceived
method, the module defines how it is going to handle any messages that are delivered to subscribed topics. In this case, the module is only subscribed to one topic, "model in". When the module initially subscribes to that topic, the configuration and device models for the module will be immediately delivered on that topic. Once received, the module will then send a message to create a simulated device if one does not already exist. Once the confirmation is received of the device's creation, then telemetry sending can begin.In summary, the code handles two potential messages: the model definitions for the module, and the confirmation of the device creation. In both cases, the module is referencing the MIT-licensed nuget package called Newtonsoft.Json to do the parsing.
The code checks to make sure the topic on which the message was received is in fact “model in” (although that should be superfluous since “model in” is the only topic to which the MQTT client is subscribed). It then checks that the type property included in the message matches the specified device type, and that the serial number matches the serial number sent in the
DeviceCreated
event.If all validation is successful, the code can then parse out the Object Id from the device’s received object model and use that to begin sending telemetry data.
var j = JObject.Parse(messageBody); //once a subscription to the "model in" topic is created, the module will //immediately receive its device and configuration models if (j["objectId"].Value<string>() == this.Configuration.ObjectId) { //create the device if needed once that happens if (string.IsNullOrWhiteSpace(_deviceSerialNumber)) { CreateDevice().Wait(); } } //handle the response once the device is created else if (j["type"] != null && j["type"].Value<string>() == DEVICE_TYPE) { var serialNumber = j["properties"]["serialNumber"]["value"] .Value<string>(); if (serialNumber.Equals(_deviceSerialNumber)) { //this is our created device _deviceId = Guid.Parse(j["objectId"].Value<string>()); Console.WriteLine("Device created confirmation " + "received. Starting telemetry..."); StartSendingTelemetryAsync(null); } }
The creation of the simulated device is handled by the
CreateDevice
method. In this method, a message is placed on the “messages out” topic that will trigger the Device Configuration Service (DCS) to create the device in the cloud and associate it with the IoT Hub instance.The device's serial number is generated using a prefix of the module's start time, so it will always be unique. The output of GenerateDeviceTags() is also passed into the CreateDeviceMessage constructor to add tags to the device to make querying easier.
The response from DCS to a
DeviceCreated
event will come back on the “model in” topic that was subscribed to in the previous step and will contain the full model definition for the created device.private async Task CreateDevice() { _deviceSerialNumber = GenerateDeviceSerialNumber(1); var topic = $"{this.Configuration.MessagesOutTopic}/type=deviceCreated"; var msg = JsonConvert.SerializeObject(new CreateDeviceMessage( DEVICE_TYPE, this.Configuration.ObjectId, _deviceSerialNumber, "devices", GenerateDeviceTags())); await _mqttClient.PublishAsync(topic, msg, MqttQualityOfServiceLevel.AtLeastOnce) .ContinueWith((e) => Console.WriteLine( $"Published message to topic '{topic}': {msg}")); }
The
StartSendingTelemetryAsync
method contains only the logic used to send the correct message for telemetry data. Notice that the message is being sent on the same “messages out” topic that was used to create the device, but now the type parameter is set to “timeSeries
” instead of “deviceCreated
” to ensure that the message is routed correctly.The code builds the telemetry message for each specific variable (they must be individual messages and cannot be concatenated into one), publishes it to the topic in a “fire and forget” manner, and then waits 10 seconds before looping. The values for these variables are randomly generated; in a production scenario, an Edge module would be receiving data from sensor devices connected to the Edge and then relaying those telemetry values in the timeSeries messages.
public async override void StartSendingTelemetryAsync(dynamic param) { Console.WriteLine("Starting timeseries publishing..."); var topic = $"{Configuration.MessagesOutTopic}/type=timeSeries"; var rand = new Random(); while (true) { foreach (var variableName in _telemetryVariables) { var msg = new TelemetryMessage<double>( _deviceId, variableName, () => Math.Round(rand.NextDouble() * 50d, 2)) .ToString(); await _mqttClient.PublishAsync(topic, msg) .ContinueWith((e) => Console.WriteLine( $"Published message to topic '{topic}': {msg}")); } //send telemetry every 10 seconds await Task.Delay(10000); } }
# Running the Sample Module
There are two ways to run this custom module. CST provides types for a given version of Ability Platform. If your CST contact has not already loaded these types into your Ability environment, these types can be manually uploaded to the environment. Once the types are loaded into the instance, any supported Edge setup can be used to run the Edge types provided by the tutorials.
Once you are familiar with the Edge, it is possible to build custom versions of the tutorials. Once a custom modules is created and the type(s) are loaded into an instance, any valid Edge can run the custom type.
# Next Steps
The Events and Alarms
tutorial builds upon topics covered here such as the
MQTT client and DeviceCreated event, and adapts the telemetry sending logic to
instead send events and alarms. It demonstrates what values can be sent with
those messages and how to specify the current state of an alarm (active or
cancelled).