繁体   English   中英

有没有办法将设备从 Azure IoT Hub 连接到 Azure IoT Central?

[英]Is there is a way to connect devices from Azure IoT Hub to Azure IoT Central?

我看到很多设备可以通过 MQTT 轻松连接到 Azure IoT 中心。 但将这些相同的设备连接到 Azure IoT Central 并不容易。 有没有办法将这些数据从 Azure IoT Hub 发送到 Azure IoT Central?

如果仅向 Azure IoT Central 应用程序发送遥测数据,您可以使用 Azure 事件网格集成器,其中设备遥测消息通过 Azure IoT 中心路由功能发布:

在此处输入图像描述

以下代码片段是用于处理所有需求(例如 DPS 等)的 webhook 订阅者实现(HttpTrigger 函数)的示例。

function.json 文件:

{
  "bindings": [
    {
      "name": "eventGridEvent",
      "authLevel": "function",
      "methods": [
        "post",
        "options"
      ],
      "direction": "in",
      "type": "httpTrigger"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ]
}

run.csx 文件:

#r "Newtonsoft.Json"

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Net;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

public static async Task<ActionResult> Run(JObject eventGridEvent, HttpRequest req, ILogger log)
{ 
    if (req.Method == HttpMethod.Options.ToString())
    {
        log.LogInformation("CloudEventSchema validation");               
        req.HttpContext.Response.Headers.Add("Webhook-Allowed-Origin", req.Headers["WebHook-Request-Origin"].FirstOrDefault()?.Trim());
        return (ActionResult)new OkResult();
    }

    // consumer of telemetry (iot central)
    uint sasTokenTTLInHrs = 1;
    string iotcScopeId = req.Headers["iotc-scopeId"].FirstOrDefault() ?? Environment.GetEnvironmentVariable("AzureIoTC_scopeId"); 
    string iotcSasToken = req.Headers["iotc-sasToken"].FirstOrDefault() ?? Environment.GetEnvironmentVariable("AzureIoTC_sasToken"); 
    log.LogInformation($"CloudEvent_Id = {eventGridEvent["id"]}"); 
    log.LogInformation($"AzureIoT_scopeId = {iotcScopeId}"); 
  
    // mandatory properties
    string source = eventGridEvent["data"]?["systemProperties"]?["iothub-message-source"]?.Value<string>();
    string deviceId = eventGridEvent["data"]?["systemProperties"]?["iothub-connection-device-id"]?.Value<string>();
                
    if (source == "Telemetry" && !string.IsNullOrEmpty(deviceId) && Regex.IsMatch(deviceId, @"^[a-z0-9\-]+$"))
    {
        var sysProp = eventGridEvent["data"]["systemProperties"];
        var appProp = eventGridEvent["data"]["properties"];
        // device model
        var component = appProp?["iothub-app-component-name"]?.Value<string>() ?? sysProp["dt-subject"]?.Value<string>() ?? "";
        var modelId = appProp?["iothub-app-model-id"]?.Value<string>() ?? sysProp["dt-dataschema"]?.Value<string>();
        // creation time
        var enqueuedtime = sysProp["iothub-enqueuedtime"]?.Value<DateTime>().ToString("o");
        var ctime = appProp?["iothub-creation-time-utc"]?.Value<DateTime>().ToString("o");
        // device group (device prefix)
        var deviceGroup = appProp?["iothub-app-device-group"]?.Value<string>();
        deviceId = $"{(deviceGroup == null ? "" : deviceGroup + "-")}{deviceId}";
        // remove sysprop
        ((JObject)eventGridEvent["data"]).Remove("systemProperties");
        
        try
        {
            var info = await Connectivity.GetConnectionInfo(deviceId, modelId, iotcScopeId, iotcSasToken, log, sasTokenTTLInHrs);
            using (HttpClient client = new HttpClient())
            {
                client.DefaultRequestHeaders.Add("Authorization", info.SasToken);
                client.DefaultRequestHeaders.Add("dt-subject", component);
                client.DefaultRequestHeaders.Add("iothub-app-iothub-creation-time-utc", ctime ?? enqueuedtime);
                var response = await client.PostAsJsonAsync(info.RequestUri, eventGridEvent["data"]);
                response.EnsureSuccessStatusCode();
            }
            log.LogInformation($"POST: {info.RequestUri}\r\n{eventGridEvent["data"]}");
        }
        catch(Exception ex)
        {
            log.LogError(ex.InnerException == null ? ex.Message : ex.InnerException.Message);
            Connectivity.RemoveDevice(deviceId);
            throw ex; // for retrying and deadlettering undeliverable message
        }
    }
    else
    {
        log.LogWarning($"Wrong event message:\r\n{eventGridEvent}");
    }
    return (ActionResult)new OkResult();
}

class ConnectivityInfo
{
    public string IoTHubName { get; set; }
    public string RequestUri { get; set; }
    public string SasToken { get; set; }
    public ulong SaSExpiry { get; set; }
    public string ModelId { get; set; }
    public string DeviceConnectionString { get; set; }
}


static class Connectivity
{
    static Dictionary<string, ConnectivityInfo> devices = new Dictionary<string, ConnectivityInfo>();

    public static async Task<ConnectivityInfo> GetConnectionInfo(string deviceId, string modelId, string iotcScopeId, string iotcSasToken, ILogger log, uint sasTokenTTLInHrs = 24, int retryCounter = 10, int pollingTimeInSeconds = 3)
    {
        if (devices.ContainsKey(deviceId))
        {
            if (!string.IsNullOrEmpty(modelId) && devices[deviceId].ModelId != modelId)
            {
                log.LogWarning($"Reprovissiong device with new model");
                devices.Remove(deviceId);
            }
            else
            {
                if (!SharedAccessSignatureBuilder.IsValidExpiry(devices[deviceId].SaSExpiry, 100))
                {
                    log.LogWarning($"Refreshing sasToken");
                    devices[deviceId].SasToken = SharedAccessSignatureBuilder.GetSASTokenFromConnectionString(devices[deviceId].DeviceConnectionString, sasTokenTTLInHrs);
                    devices[deviceId].SaSExpiry = ulong.Parse(SharedAccessSignatureBuilder.GetExpiry(sasTokenTTLInHrs));
                }
                return devices[deviceId];
            }
        }

        string deviceKey = SharedAccessSignatureBuilder.ComputeSignature(iotcSasToken, deviceId);
        string address = $"https://global.azure-devices-provisioning.net/{iotcScopeId}/registrations/{deviceId}/register?api-version=2021-06-01";
        string sas = SharedAccessSignatureBuilder.GetSASToken($"{iotcScopeId}/registrations/{deviceId}", deviceKey, "registration", 1);

        using (HttpClient client = new HttpClient())
        {
            client.DefaultRequestHeaders.Add("Authorization", sas);
            client.DefaultRequestHeaders.Add("accept", "application/json");
            string jsontext = string.IsNullOrEmpty(modelId) ? null : $"{{ \"modelId\":\"{modelId}\" }}";
            var response = await client.PutAsync(address, new StringContent(JsonConvert.SerializeObject(new { registrationId = deviceId, payload = jsontext }), Encoding.UTF8, "application/json"));

            var atype = new { errorCode = "", message = "", operationId = "", status = "", registrationState = new JObject() };
            do
            {
                dynamic operationStatus = JsonConvert.DeserializeAnonymousType(await response.Content.ReadAsStringAsync(), atype);
                if (!string.IsNullOrEmpty(operationStatus.errorCode))
                {
                    throw new Exception($"{operationStatus.errorCode} - {operationStatus.message}");
                }
                response.EnsureSuccessStatusCode();
                if (operationStatus.status == "assigning")
                {
                    Task.Delay(TimeSpan.FromSeconds(pollingTimeInSeconds)).Wait();
                    address = $"https://global.azure-devices-provisioning.net/{iotcScopeId}/registrations/{deviceId}/operations/{operationStatus.operationId}?api-version=2021-06-01";
                    response = await client.GetAsync(address);
                }
                else if (operationStatus.status == "assigned")
                {
                    var cinfo = new ConnectivityInfo();
                    cinfo.ModelId = modelId;
                    cinfo.IoTHubName = operationStatus.registrationState.assignedHub;
                    cinfo.DeviceConnectionString = $"HostName={cinfo.IoTHubName};DeviceId={deviceId};SharedAccessKey={deviceKey}";
                    cinfo.RequestUri = $"https://{cinfo.IoTHubName}/devices/{deviceId}/messages/events?api-version=2021-04-12";
                    cinfo.SasToken = SharedAccessSignatureBuilder.GetSASToken($"{cinfo.IoTHubName}/{deviceId}", deviceKey, null, sasTokenTTLInHrs);
                    cinfo.SaSExpiry = ulong.Parse(SharedAccessSignatureBuilder.GetExpiry(sasTokenTTLInHrs));
                    devices.Add(deviceId, cinfo);
                    log.LogInformation($"DeviceConnectionString: {cinfo.DeviceConnectionString}");                        
                    return cinfo;
                }
                else
                {
                    throw new Exception($"{operationStatus.registrationState.status}: {operationStatus.registrationState.errorCode} - {operationStatus.registrationState.errorMessage}");
                }
            } while (--retryCounter > 0);

            throw new Exception("Registration device status retry timeout exprired, try again.");
        }
    }

    public static void RemoveDevice(string deviceId)
    {
        if (devices.ContainsKey(deviceId))
            devices.Remove(deviceId);
    }
}

public sealed class SharedAccessSignatureBuilder
{
    public static string GetHostNameNamespaceFromConnectionString(string connectionString)
    {
        return GetPartsFromConnectionString(connectionString)["HostName"].Split('.').FirstOrDefault();
    }
    public static string GetSASTokenFromConnectionString(string connectionString, uint hours = 24)
    {
        var parts = GetPartsFromConnectionString(connectionString);
        if (parts.ContainsKey("HostName") && parts.ContainsKey("SharedAccessKey"))
            return GetSASToken(parts["HostName"], parts["SharedAccessKey"], parts.Keys.Contains("SharedAccessKeyName") ? parts["SharedAccessKeyName"] : null, hours);
        else
            return string.Empty;
    }
    public static string GetSASToken(string resourceUri, string key, string keyName = null, uint hours = 24)
    {
        try
        {
            var expiry = GetExpiry(hours);
            string stringToSign = System.Web.HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
            var signature = SharedAccessSignatureBuilder.ComputeSignature(key, stringToSign);
            var sasToken = keyName == null ?
                String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", System.Web.HttpUtility.UrlEncode(resourceUri), System.Web.HttpUtility.UrlEncode(signature), expiry) :
                String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", System.Web.HttpUtility.UrlEncode(resourceUri), System.Web.HttpUtility.UrlEncode(signature), expiry, keyName);
            return sasToken;
        }
        catch
        {
            return string.Empty;
        }
    }

    #region Helpers
    public static string ComputeSignature(string key, string stringToSign)
    {
        using (HMACSHA256 hmac = new HMACSHA256(Convert.FromBase64String(key)))
        {
            return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
        }
    }

    public static Dictionary<string, string> GetPartsFromConnectionString(string connectionString)
    {
        return connectionString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Split(new[] { '=' }, 2)).ToDictionary(x => x[0].Trim(), x => x[1].Trim(), StringComparer.OrdinalIgnoreCase);
    }

    // default expiring = 24 hours
    public static string GetExpiry(uint hours = 24)
    {
        TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
        return Convert.ToString((ulong)sinceEpoch.TotalSeconds + 3600 * hours);
    }

    public static DateTime GetDateTimeUtcFromExpiry(ulong expiry)
    {
        return (new DateTime(1970, 1, 1)).AddSeconds(expiry);
    }
    public static bool IsValidExpiry(ulong expiry, ulong toleranceInSeconds = 0)
    {
        return GetDateTimeUtcFromExpiry(expiry) - TimeSpan.FromSeconds(toleranceInSeconds) > DateTime.UtcNow;
    }
    #endregion
}

以下屏幕片段显示了为 webhook 订阅者传递请求标头的订阅的一部分:

在此处输入图像描述

请注意,基于设备 model,可以在输入端的 Azure IoT Central App 中使用映射功能。

如上图所示,此解决方案基于使用 Azure 事件网格功能,其中 Azure IoT 中心代表设备遥测数据的发布者,而 Azure IoT Central 应用程序是其消费者。

Azure IoT 中心和 Azure IoT Central 之间的逻辑连接是通过 AEG 订阅和一个 webhook 目标处理程序完成的,例如HttpTrigger Function(参见上面的实现)。 请注意,此订阅配置为在CloudEventSchema中传递事件消息(设备遥测数据)。

通过 DPS 在 IoTHub 中预配自身的设备将与 IoT Central 一起工作,除了设备在预配期间发送的标识 DPS 服务实例的 ID Scope 之外没有任何变化。 一个 ID Scope 将指向在 DPS 注册组中配置的特定 IoT 中心,而另一个将指向 IoT Central 应用程序中的内部 IoT 中心(IoT Central 根据需要旋转额外的内部 IoT 中心以进行自动缩放,这就是为什么它有自己的原因内部 DPS)。

使用 DPS 允许在第一次调用时将设备配置到特定的 IoTHub,随后可以显式触发更改以重新配置到不同的 IoTHub 或 IoT Central,如果需要可用于移动设备。 此功能允许您通过实施 ID Scope 更改直接方法并触发重新配置来强制设备连接到 IoT 中心或 IoT Central 的场景。 强烈建议使用 DPS,因为它可以简化配置并提供这种灵活性。

重新配置应该是设备重试逻辑的一部分,以防除了上述按需更改之外,如果它在一定时间内无法连接到 IoTHub。

是什么让您想到“但将这些相同的设备连接到 Azure IoT Central 并不容易”?

连接到 IoTHub 的任何设备也可以连接到 IoTCentral,您只需使用 DPS 预配设备,它将获得 IoTHub 主机名,其他一切都将以相同的方式工作。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM