Um die Funktion des zuvor angelegten Device Provisioning Service zu testen, werden wir zum einen ein simuliertes IoT-Gerät verwenden und anschließend im nächsten Artikel auch einen ESP32. Der Code ist einfach aufgebaut aufgebaut und überschaubar. Es ist sehr einfach möglich den Code von oben nach unten zu lesen und zu verstehen was dort passiert. Ich werde die wichtigsten Parameter erklären die gesetzt werden müssen und wo man diese im Azure Portal erhält. Anschließend werde ich kurz die einzelnen Code-Abschnitte erklären.

Unser simuliertes IoT-Gerät ist eine Microsoft .NET Core Konsolenanwendung in C# geschrieben. Der Code wurde im Original gepostet auf GitHub MicrosoftLearning/AZ-220ZH-Microsoft-Azure-IoT-Developer … /… /06-Automatic Enrollment of Devices in DPS/Final/ContainerDevice/Program.cs.

Wir starten Visual Studio und erstellen ein neues Konsolen-App (.NET Core) Projekt.

Anschließend kopieren wir den Code der Datei Program.cs aus der GitHub Repository in die Program.cs Datei im Projekt. Damit die Paket-Konflikte behoben werden, müssen wir die nachfolgenden Pakete mit Hilfe des NuGet-Paketmanagers installieren.

Nuget-Packages

Nachfolgend die NuGet-Pakete als Install-Package Befehle, so dass diese hier direkt kopiert werden können.

Install-Package Microsoft.Azure.Devices.Client -Version 1.35.0
Install-Package Microsoft.Azure.Devices.Provisioning.Client -Version 1.16.3
Install-Package Microsoft.Azure.Devices.Provisioning.Transport.Mqtt -Version 1.14.0
Install-Package Microsoft.Azure.Devices.Provisioning.Transport.Amqp -Version 1.14.0-preview-001
Install-Package Microsoft.Azure.Devices.Provisioning.Transport.Http -Version 1.13.0-preview-001
Install-Package Newtonsoft.Json -Version 13.0.1-beta2

Danach müssen wir das IoT-Gerätezertifkat, was wir im vorherigen Artikel Der Microsoft Azure Device Provisioning Service im letzten Schritt mit dem IoT-Gerätenamen fügen wir per drag and drop hinzu. Diese Zertifikatsdatei fügen wir, beispielsweise per Drag and Drop, unserem Projekt hinzu.

Der Code

using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Provisioning.Client;
using Microsoft.Azure.Devices.Provisioning.Client.Transport;
using Microsoft.Azure.Devices.Shared;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Security.Cryptography.X509Certificates;
namespace IoTGarage_Azure_05_DPS_x509_DeviceProvisioning
{
    class Program
    {
        // Azure Device Provisioning Service (DPS) ID Scope
        private static string dpsIdScope = "0ne00XXA1XX";
        // Certificate (PFX) File Name
        private static string certificateFileName = "bogxiotdevice1000.pfx";
        // Certificate (PFX) Password
        private static string certificatePassword = "1234";
        // NOTE: For the purposes of this example, the certificatePassword is
        // hard coded. In a production device, the password will need to be stored
        // in a more secure manner. Additionally, the certificate file (PFX) should
        // be stored securely on a production device using a Hardware Security Module.
        private const string GlobalDeviceEndpoint = "global.azure-devices-provisioning.net";
        private static int telemetryDelay = 1;
        private static DeviceClient deviceClient;
        // INSERT Main method below here
        public static async Task Main(string[] args)
        {
            X509Certificate2 certificate = LoadProvisioningCertificate();
            using (var security = new SecurityProviderX509Certificate(certificate))
            using (var transport = new ProvisioningTransportHandlerAmqp(TransportFallbackType.TcpOnly))
            {
                ProvisioningDeviceClient provClient =
                    ProvisioningDeviceClient.Create(GlobalDeviceEndpoint, dpsIdScope, security, transport);
                using (deviceClient = await ProvisionDevice(provClient, security))
                {
                    await deviceClient.OpenAsync().ConfigureAwait(false);
                    // INSERT Setup OnDesiredPropertyChanged Event Handling below here
                    await deviceClient.SetDesiredPropertyUpdateCallbackAsync(OnDesiredPropertyChanged, null).ConfigureAwait(false);
                    // INSERT Load Device Twin Properties below here
                    var twin = await deviceClient.GetTwinAsync().ConfigureAwait(false);
                    await OnDesiredPropertyChanged(twin.Properties.Desired, null);
                    // Start reading and sending device telemetry
                    Console.WriteLine("Start reading and sending device telemetry...");
                    await SendDeviceToCloudMessagesAsync();
                    await deviceClient.CloseAsync().ConfigureAwait(false);
                }
            }
        }
        // INSERT LoadProvisioningCertificate method below here
        private static X509Certificate2 LoadProvisioningCertificate()
        {
            var certificateCollection = new X509Certificate2Collection();
            certificateCollection.Import(certificateFileName, certificatePassword, X509KeyStorageFlags.UserKeySet);
            X509Certificate2 certificate = null;
            foreach (X509Certificate2 element in certificateCollection)
            {
                Console.WriteLine($"Found certificate: {element?.Thumbprint} {element?.Subject}; PrivateKey: {element?.HasPrivateKey}");
                if (certificate == null && element.HasPrivateKey)
                {
                    certificate = element;
                }
                else
                {
                    element.Dispose();
                }
            }
            if (certificate == null)
            {
                throw new FileNotFoundException($"{certificateFileName} did not contain any certificate with a private key.");
            }
            Console.WriteLine($"Using certificate {certificate.Thumbprint} {certificate.Subject}");
            return certificate;
        }
        // INSERT ProvisionDevice method below here
        private static async Task<DeviceClient> ProvisionDevice(ProvisioningDeviceClient provisioningDeviceClient, SecurityProviderX509Certificate security)
        {
            var result = await provisioningDeviceClient.RegisterAsync().ConfigureAwait(false);
            Console.WriteLine($"ProvisioningClient AssignedHub: {result.AssignedHub}; DeviceID: {result.DeviceId}");
            if (result.Status != ProvisioningRegistrationStatusType.Assigned)
            {
                throw new Exception($"DeviceRegistrationResult.Status is NOT 'Assigned'");
            }
            var auth = new DeviceAuthenticationWithX509Certificate(
                result.DeviceId,
                security.GetAuthenticationCertificate());
            return DeviceClient.Create(result.AssignedHub, auth, TransportType.Amqp);
        }
        private static async Task SendDeviceToCloudMessagesAsync()
        {
            var sensor = new EnvironmentSensor();
            while (true)
            {
                var currentTemperature = sensor.ReadTemperature();
                var currentHumidity = sensor.ReadHumidity();
                var currentPressure = sensor.ReadPressure();
                var currentLocation = sensor.ReadLocation();
                var messageString = CreateMessageString(currentTemperature,
                                                        currentHumidity,
                                                        currentPressure,
                                                        currentLocation);
                var message = new Message(Encoding.ASCII.GetBytes(messageString));
                // Add a custom application property to the message.
                // An IoT hub can filter on these properties without access to the message body.
                message.Properties.Add("temperatureAlert", (currentTemperature > 30) ? "true" : "false");
                // Send the telemetry message
                await deviceClient.SendEventAsync(message);
                Console.WriteLine("{0} > Sending message: {1}", DateTime.Now, messageString);
                // Delay before next Telemetry reading
                await Task.Delay(telemetryDelay * 1000);
            }
        }
        private static string CreateMessageString(double temperature, double humidity, double pressure, EnvironmentSensor.Location location)
        {
            // Create an anonymous object that matches the data structure we wish to send
            var telemetryDataPoint = new
            {
                temperature = temperature,
                humidity = humidity,
                pressure = pressure,
                latitude = location.Latitude,
                longitude = location.Longitude
            };
            var messageString = JsonConvert.SerializeObject(telemetryDataPoint);
            // Create a JSON string from the anonymous object
            return JsonConvert.SerializeObject(telemetryDataPoint);
        }
        // INSERT OnDesiredPropertyChanged method below here
        private static async Task OnDesiredPropertyChanged(TwinCollection desiredProperties, object userContext)
        {
            Console.WriteLine("Desired Twin Property Changed:");
            Console.WriteLine($"{desiredProperties.ToJson()}");
            // Read the desired Twin Properties
            if (desiredProperties.Contains("telemetryDelay"))
            {
                string desiredTelemetryDelay = desiredProperties["telemetryDelay"];
                if (desiredTelemetryDelay != null)
                {
                    telemetryDelay = int.Parse(desiredTelemetryDelay);
                }
                // if desired telemetryDelay is null or unspecified, don't change it
            }
            // Report Twin Properties
            var reportedProperties = new TwinCollection();
            reportedProperties["telemetryDelay"] = telemetryDelay.ToString();
            await deviceClient.UpdateReportedPropertiesAsync(reportedProperties).ConfigureAwait(false);
            Console.WriteLine("Reported Twin Properties:");
            Console.WriteLine($"{reportedProperties.ToJson()}");
        }
    }
    internal class EnvironmentSensor
    {
        // Initial telemetry values
        double minTemperature = 20;
        double minHumidity = 60;
        double minPressure = 1013.25;
        double minLatitude = 39.810492;
        double minLongitude = -98.556061;
        Random rand = new Random();
        internal class Location
        {
            internal double Latitude;
            internal double Longitude;
        }
        internal double ReadTemperature()
        {
            return minTemperature + rand.NextDouble() * 15;
        }
        internal double ReadHumidity()
        {
            return minHumidity + rand.NextDouble() * 20;
        }
        internal double ReadPressure()
        {
            return minPressure + rand.NextDouble() * 12;
        }
        internal Location ReadLocation()
        {
            return new Location { Latitude = minLatitude + rand.NextDouble() * 0.5, Longitude = minLongitude + rand.NextDouble() * 0.5 };
        }
    }
}

Im Code gibt es vier wichtige Konstanten die angepasst werden müssen:

  • dpsIdScope: ID Scope des Device Provisioning Services
  • certificateFilename: Die Zertifikatsdatei
  • certificatePassword: Das Zertifikatspasswort was wir in der PowerShell während der Generierung des Zertifikats festgelegt haben
  • GlobalDeviceEndpoint: Der globale Device Endpunkt

Den Azure Device Provisioning Service ID Scope finden wir im Azure Portal. Öffnen dazu im Azure Portal unseren Device Provisioning Service iotgdps und in der Übersicht nehmen wir den Wert ID Scope und weisen diesen Wert der Konstante dpsIdScope zu.

Den Namen der Zertifikatsdatei, in unserem Fall bogxiotdevice1000.pfx, weisen wir der Konstante certificateFileName zu. Das Passwort, dass wir während der Generierung des Zertifikats vergeben haben, weisen wir der Konstante certificatePassword zu.

Für die letzte zu befüllende Konstante GlobalDeviceEndpoint nehmen wir den Wert Global device endpoint aus der Übersichtsseite des Device Provisioning Service im Azure Portal.

Damit sind fertig. Um zu schauen, ob das was wir erstellt und angepasst haben auch funktioniert, führen wir die Anwendung aus und sehen, dass unserem Client ein IoT Hub sowie eine Geräte-ID zugewiesen wurde. Anschließend beginnt das simulierte Gerät mit dem Senden von simulierten Telemetriedaten.

Um zu schauen, ob unser simuliertes Gerät auch im IoT Hub registriert wurde, öffnen wir im Azure Portal unseren IoT Hub iotghub. Im linken Menü wählen wir den Punkt Explorers > IoT devices und sollten nun auf der rechten Seite in der Liste der IoT-Geräte uns simuliertes Gerät mit der ID bogxiotdevice1000 sehen.

In der Enrollment Group im Device Provisioning Service sollte das simulierte Gerät auch zu finden sein. Dazu öffnen wir im Azure Portal unseren Device Provisioning Service und klicken im linken Menü auf Settings > Manage enrollments. Rechts im Fenster klicken wir dann auf den Reiter Enrollment Groups und wählen dann in der Liste die Enrollment Group bogxgroup1-rootca.

In der Enrollment Group Details Ansicht klicken wir auf den Reiter Registration Records.

Auch hier erscheint in der Liste unser simuliertes Gerät mit der Id bogxiotdevice1000.

Damit funktioniert unser Device Provisioning Service sowie unsere Zertifikatskette. Im nächsten Artikel werden wir einen ESP32 via Device Provisioning Service registrieren.