How to generate custom schema complex telemetry data for a IoT Central Device using C#?

GuidoL 310 Reputation points
2023-05-17T17:52:58.0366667+00:00

Hi,

in analogy with a similar question of which I got answer, i would like to deepen about how to generate custom schema complex telemetry for a IoT Central Device using C# for my project.

I have an IoT Central device template that has a schema complex telemetry like the following:

{
    "@id": "dtmi:digitaltwins:org:archive:download:rfidsmartlibrary:RfidSmartLibraryowl:LibraryItemGateCheckSensorTest;1",
    "@type": "Interface",
    "contents": [
        {
            "@id": "dtmi:digitaltwins:org:archive:download:rfidsmartlibrary:RfidSmartLibraryowl:LibraryItemGateCheckSensorTest:LibraryItemData;1",
            "@type": "Telemetry",
            "displayName": {
                "en": "LibraryItemData"
            },
            "name": "LibraryItemData",
            "schema": {
                "@id": "dtmi:digitaltwins:org:archive:download:rfidsmartlibrary:RfidSmartLibraryowl:LibraryItemGateCheckSensorTest:LibraryItemData:schema;1",
                "@type": "Object",
                "displayName": {
                    "en": "Object"
                },
                "fields": [
                    {
                        "@id": "dtmi:digitaltwins:org:archive:download:rfidsmartlibrary:RfidSmartLibraryowl:LibraryItemGateCheckSensorTest:LibraryItemData:schema:ItemId;1",
                        "displayName": {
                            "en": "ItemId"
                        },
                        "name": "ItemId",
                        "schema": "string"
                    },
                    {
                        "@id": "dtmi:digitaltwins:org:archive:download:rfidsmartlibrary:RfidSmartLibraryowl:LibraryItemGateCheckSensorTest:LibraryItemData:schema:Description;1",
                        "displayName": {
                            "en": "Description"
                        },
                        "name": "Description",
                        "schema": "string"
                    },
                    {
                        "@id": "dtmi:digitaltwins:org:archive:download:rfidsmartlibrary:RfidSmartLibraryowl:LibraryItemGateCheckSensorTest:LibraryItemData:schema:Location;1",
                        "displayName": {
                            "en": "Location"
                        },
                        "name": "Location",
                        "schema": "string"
                    },
                    {
                        "@id": "dtmi:digitaltwins:org:archive:download:rfidsmartlibrary:RfidSmartLibraryowl:LibraryItemGateCheckSensorTest:LibraryItemData:schema:DateTimeValue;1",
                        "displayName": {
                            "en": "DateTimeValue"
                        },
                        "name": "DateTimeValue",
                        "schema": "dateTime"
                    }
                ]
            }
        }
    ],
    "displayName": {
        "en": "LibraryItemGateCheckSensorTest"
    },
    "@context": [
        "dtmi:iotcentral:context;2",
        "dtmi:dtdl:context;2"
    ]
}

and i need to send telemetry data using c#; following an example of TemperatureController https://github.com/Azure/azure-iot-sdk-csharp/tree/main/iothub/device/samples/solutions/PnpDeviceSamples/TemperatureController

i developed a LibraryDataDeviceController.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.ConstrainedExecution;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Provisioning;
//using Microsoft.Azure.Devices.Provisioning.Client.PlugAndPlay;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using PnpHelpers;
namespace Microsoft.Azure.Devices.Client.Samples
{
    public class LibraryDataDeviceController
    {
        private readonly DeviceClient _deviceClient;
        private readonly ILogger _logger;
       
        List<string> ItemID = new List<string>() {
            "USB 2364",
            "USB 2365",
            "USB 2366", 
            "USB 2367",
            "USB 2370",
            "USB 2373",
            "USB 2375",
            "USB 2376",
            "USB 2501",
            "USB 2606"
        };
        
        List<string> Description = new List<string>() { 
            "*Re-inventare la famiglia : guida teorico-pratica per i professionisti dell'educazione / a cura di Laura Formenti Santarcangelo di Romagna : Maggioli, 2016.. XXII, 451 p. ; 21 cm",
            "Le *parole dell'etica / Antonio Da Re [Milano] : Bruno Mondadori, 2010 XII, 215 p. ; 21 cm",
            "*Teoria e prassi in pedagogia : questioni epistemologiche / a cura di Massimo Baldacci ed Enza Colicchi Roma : Carocci, 2016 258 p. ; 22 cm",
            "*Cantar ottave : per una storia culturale dell'intonazione cantata in ottava rima / a cura di Maurizio Agamennone Lucca : Libreria musicale italiana, 2017 XXVI, 184 p. ; 24 cm",
            "*sacré dans la vie quotidienne : suivi de Notes pour Le sacré dans la vie quotidienne ou L'homme sans honneur / Michel Leiris ; présentation et notes de Denis Hollier ; préface de Lionel Menasché Paris : Allia, 2018 140 p. ; 17 cm",
            "*Didattica e intercultura / Donatello Santarone Roma : Armando, 2012 127 p. ; 20 cm",
            "*Teorie e metodi di pedagogia interculturale / Mariangela Giusti Bari ; Roma : Laterza, 2017 VI, 188 p. ; 21 cm",
            "L'*italiano accademico : uno studio sulla glottodidattica dell'italiano lingua di studio all'università a studenti in mobilità internazionale / Elena Ballarin Saarbrücken : Edizioni Accademiche Italiane, 2017 243 p. : ill. ; 22 cm",
            "*Empirismo e soggettività : Saggio sulla natura umana secondo Hume / Gilles Deleuze ; a cura di Adriano Vinale. - 2. Ed Napoli : Cronopio , 2012 174 p. ; 21 cm",
            "La *vita delle piante : metafisica della mescolanza / Emanuele Coccia Bologna : Il mulino, 2018 159 p. ; 21 cm"};
        
        List<string> Location = new List<string>() { 
            "Main Gate" };
        //Random rnd = new Random();
        public LibraryDataDeviceController(DeviceClient deviceClient, ILogger logger)
        {
            _deviceClient = deviceClient ?? throw new ArgumentNullException(nameof(deviceClient));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }
        public async Task PerformOperationsAsync(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                await SendLibraryDataAsync(cancellationToken);
                Thread.Sleep(30000);
                await Task.Delay(5 * 1000, cancellationToken);
            }
        }
        private async Task SendLibraryDataAsync(CancellationToken cancellationToken)
        {
            string telemetryName = "LibraryItemData";
            DateTime time = DateTime.Now;
            
            Random rnd = new Random();
            int index = rnd.Next(0,10);
            
            using Message msg = PnpConvention.CreateMessage(telemetryName, ItemID[index] + "," + "Description:" + Description[index] + "," + "Location:" + Location[0] + "," + "DateTime:" + time);
            await _deviceClient.SendEventAsync(msg, cancellationToken);
            _logger.LogDebug($"Telemetry: Sent - {{ \"{telemetryName}\": {"LibraryItemData"} }}.");
           
	 }
    }
}

and used the program.cs that has Iot Central Device Template ModelId for my template with complex schema.

using CommandLine;
using Microsoft.Azure.Devices.Logging;
using Microsoft.Azure.Devices.Provisioning.Client;
using Microsoft.Azure.Devices.Provisioning.Client.PlugAndPlay;
using Microsoft.Azure.Devices.Provisioning.Client.Transport;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Azure.Devices.Client.Samples
{
    public class Program
    {
        
        // Iot Central Device Template ModelId  
        private const string ModelId = "dtmi:digitaltwins:org:archive:download:rfidsmartlibrary:RfidSmartLibraryowl:LibraryItemGateCheckSensorTest;1";
      
        private const string SdkEventProviderPrefix = "Microsoft-Azure-";
        public static async Task Main(string[] args)
        {
            // Parse application parameters
            Parameters parameters = null;
            ParserResult<Parameters> result = Parser.Default.ParseArguments<Parameters>(args)
                .WithParsed(parsedParams =>
                {
                    parameters = parsedParams;
                })
                .WithNotParsed(errors =>
                {
                    Environment.Exit(1);
                });
            // Set up logging
            ILogger logger = InitializeConsoleDebugLogger();
            // Instantiating this seems to do all we need for outputting SDK events to our console log.
            using var sdkLog = new ConsoleEventListener(SdkEventProviderPrefix, logger);
            if (!parameters.Validate(logger))
            {
                Console.WriteLine(CommandLine.Text.HelpText.AutoBuild(result, null, null));
                Environment.Exit(1);
            }
            using var cts = parameters.ApplicationRunningTime.HasValue
                ? new CancellationTokenSource(TimeSpan.FromSeconds(parameters.ApplicationRunningTime.Value))
                : new CancellationTokenSource();
            logger.LogInformation("Press Control+C to quit the sample.");
            Console.CancelKeyPress += (sender, eventArgs) =>
            {
                eventArgs.Cancel = true;
                cts.Cancel();
                logger.LogInformation("Sample execution cancellation requested; will exit.");
            };
            logger.LogDebug($"Set up the device client.");
            try
            {
                using DeviceClient deviceClient = await SetupDeviceClientAsync(parameters, logger, cts.Token);
                var sample = new LibraryDataDeviceController(deviceClient, logger);
                await sample.PerformOperationsAsync(cts.Token);
                // PerformOperationsAsync is designed to run until cancellation has been explicitly requested, either through
                // cancellation token expiration or by Console.CancelKeyPress.
                // As a result, by the time the control reaches the call to close the device client, the cancellation token source would
                // have already had cancellation requested.
                // Hence, if you want to pass a cancellation token to any subsequent calls, a new token needs to be generated.
            
                await deviceClient.CloseAsync(CancellationToken.None);
            }
            catch (Exception ex) when (ex is OperationCanceledException || ex is ProvisioningTransportException)
            {
                // User canceled the operation. Nothing to do here.
            }
        }
        private static ILogger InitializeConsoleDebugLogger()
        {
            using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
            {
                builder
                .AddFilter(level => level >= LogLevel.Debug)
                .AddSystemdConsole(options =>
                {
                    options.TimestampFormat = "[MM/dd/yyyy HH:mm:ss]";
                });
            });
            return loggerFactory.CreateLogger<LibraryDataDeviceController>();
        }
        private static async Task<DeviceClient> SetupDeviceClientAsync(Parameters parameters, ILogger logger, CancellationToken cancellationToken)
        {
            DeviceClient deviceClient;
            switch (parameters.DeviceSecurityType.ToLowerInvariant())
            {
                case "dps":
                    logger.LogDebug($"Initializing via DPS");
                    DeviceRegistrationResult dpsRegistrationResult = await ProvisionDeviceAsync(parameters, cancellationToken);
                    var authMethod = new DeviceAuthenticationWithRegistrySymmetricKey(dpsRegistrationResult.DeviceId, parameters.DeviceSymmetricKey);
                    deviceClient = InitializeDeviceClient(dpsRegistrationResult.AssignedHub, authMethod);
                    break;
                case "connectionstring":
                    logger.LogDebug($"Initializing via IoT hub connection string");
                    deviceClient = InitializeDeviceClient(parameters.PrimaryConnectionString);
                    break;
                default:
                    throw new ArgumentException($"Unrecognized value for device provisioning received: {parameters.DeviceSecurityType}." +
                        $" It should be either \"dps\" or \"connectionString\" (case-insensitive).");
            }
            return deviceClient;
        }
        // Provision a device via DPS, by sending the PnP model Id as DPS payload.
        private static async Task<DeviceRegistrationResult> ProvisionDeviceAsync(Parameters parameters, CancellationToken cancellationToken)
        {
            using SecurityProvider symmetricKeyProvider = new SecurityProviderSymmetricKey(parameters.DeviceId, parameters.DeviceSymmetricKey, null);
            using ProvisioningTransportHandler mqttTransportHandler = new ProvisioningTransportHandlerMqtt();
            ProvisioningDeviceClient pdc = ProvisioningDeviceClient.Create(parameters.DpsEndpoint, parameters.DpsIdScope, symmetricKeyProvider, mqttTransportHandler);
            var pnpPayload = new ProvisioningRegistrationAdditionalData
            {
                JsonData = PnpConvention.CreateDpsPayload(ModelId),
            };
            return await pdc.RegisterAsync(pnpPayload, cancellationToken);
        }
        // Initialize the device client instance using connection string based authentication, over Mqtt protocol (TCP, with fallback over Websocket) and
        // setting the ModelId into ClientOptions.
        private static DeviceClient InitializeDeviceClient(string deviceConnectionString)
        {
            var options = new ClientOptions
            {
                ModelId = ModelId,
            };
            DeviceClient deviceClient = DeviceClient.CreateFromConnectionString(deviceConnectionString, TransportType.Mqtt, options);
            return deviceClient;
        }
        // Initialize the device client instance using symmetric key based authentication, over Mqtt protocol (TCP, with fallback over Websocket)
        // and setting the ModelId into ClientOptions.
        private static DeviceClient InitializeDeviceClient(string hostname, IAuthenticationMethod authenticationMethod)
        {
            var options = new ClientOptions
            {
                ModelId = ModelId,
            };
            DeviceClient deviceClient = DeviceClient.Create(hostname, authenticationMethod, TransportType.Mqtt, options);
            return deviceClient;
        }
    }
}

so it works but i'm not sure if code and data model are correct; i ask, if it's possible, a code example for such a template and a method to manage template like this.

is it possible to start from a c# model like the one i posted to generate custom telemetry data using a C# program? Consider that device data automatically generated by IoT Central have no meaning! I hope i was clear.

Thanks in advance.

Guido

Azure IoT Central
Azure IoT Central
An Azure hosted internet of things (IoT) application platform.
376 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Dominic 1,631 Reputation points Microsoft Employee
    2023-06-05T09:30:40.46+00:00

    If I've understood correctly, you're looking for a way to automatically generate the C# code that sends the telemetry to IoT Central from a device template?

    I'm not aware of any tools that do this, but the guides here show how to generate telemetry for a wide variety of device template telemetry types, including complex types:

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.