使用 HTTP POST 请求将遥测消息发送到 Azure IoT Central 设备

Send telemetry messages to Azure IoT Central device using HTTP POST request

我正在尝试使用 HTTP POST 请求将遥测数据发送到 Azure IoT Central 中的设备。

类似的 Rest API 可用于 Azure IoT 中心 - https://docs.microsoft.com/en-us/rest/api/iothub/device/send-device-event

我能够使用此网站提取 Azure IoT Central 背后的 IoT 中心资源 URL - https://dpsgen.z8.web.core.windows.net/

它采用我们从 Azure IoT Central 获得的范围 ID、设备 ID 和设备主键。它为您提供 IoT 中心连接字符串,

HostName=iotc-<<unique-iot-hub-id>>.azure-devices.net;DeviceId=<<device-id>>;SharedAccessKey=<<device-primary-key>>

使用上面的 IoT 中心主机名,我尝试了 IoT 中心发送设备事件 Rest API。它因未授权错误而失败。

我正在使用从 Azure IoT Central 应用程序中的以下路径生成的 SAS 令牌

Azure IoT Central -> Permissions -> API tokens -> "App Administrator" Role

任何帮助都会有用。

IoT Central API 令牌用于管理应用程序功能,设备无法使用。 Select IoT Central 中的设备并单击顶部的“连接”菜单,使用为该设备显示的主键。

附带说明一下,尽管 https 受设备支持,但由于其轮询性质而不适合 IoT,并且不支持设备孪生所需或报告的属性。 https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-d2c-guidance

IoT Central 提供 built-in 高可用性,底层 IoTHub 名称可以更改,因此不建议手动获取 IoTHub 名称。始终调用 DPS 以检索 IoTHub 名称、第一次和定期或错误情况。

看看我的answer,其中详细描述了如何使用 REST Post 请求生成连接信息以将遥测数据发送到 Azure IoT Central 应用程序。

以下是更新的 azure 函数,用于生成请求的设备连接信息:

#r "Newtonsoft.Json"

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
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;
using System.Web;


public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
    int retryCounter = 10;
    int pollingTimeInSeconds = 3;

    string deviceId = req.Query["deviceid"];
    string mid = req.Query["modelid"];
               
    log.LogInformation($"DeviceId = {deviceId}, ModelId = {mid}");

    if (!Regex.IsMatch(deviceId, @"^[a-z0-9\-]+$"))
        throw new Exception($"Invalid format: DeviceID must be alphanumeric, lowercase, and may contain hyphens");

    string iotcScopeId = System.Environment.GetEnvironmentVariable("AzureIoTC_scopeId");      
    string iotcSasToken = System.Environment.GetEnvironmentVariable("AzureIoTC_sasToken");    
            
    if(string.IsNullOrEmpty(iotcScopeId) || string.IsNullOrEmpty(iotcSasToken))
        throw new ArgumentNullException($"Missing the scopeId and/or sasToken of the IoT Central App");

    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");
 
    log.LogInformation($"sas_token: {sas}");

    using (HttpClient client = new HttpClient())
    {
        client.DefaultRequestHeaders.Add("Authorization", sas);
        client.DefaultRequestHeaders.Add("accept", "application/json");
        string jsontext = string.IsNullOrEmpty(mid) ? null : $"{{ \"modelId\": \"{mid}\" }}";
        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")
            {
                log.LogInformation($"{JsonConvert.SerializeObject(operationStatus, Formatting.Indented)}");
                string assignedHub = operationStatus.registrationState.assignedHub;
                string cstr = $"HostName={assignedHub};DeviceId={operationStatus.registrationState.deviceId};SharedAccessKey={deviceKey}"; // + (string.IsNullOrEmpty(mid) ? "" : $";modelId={mid.Replace(";","#")}"); 
                string requestUrl = $"https://{assignedHub}/devices/{operationStatus.registrationState.deviceId}/messages/events?api-version=2021-04-12";
                string deviceSasToken = SharedAccessSignatureBuilder.GetSASToken($"{assignedHub}/{operationStatus.registrationState.deviceId}", deviceKey);

                log.LogInformation($"IoTC DeviceConnectionString:\n\t{cstr}");
                return new OkObjectResult(JObject.FromObject(new { iotHub = assignedHub, iotFireUrl = requestUrl, deviceSasToken = deviceSasToken, deviceConnectionString = cstr }));
            }
            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 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}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry) :
                String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUri), 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
    private 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;
    }

    public static string CreateSHA256Key(string secret)
    {
        using (var provider = new SHA256CryptoServiceProvider())
        {
            byte[] keyArray = provider.ComputeHash(UTF8Encoding.UTF8.GetBytes(secret));
            provider.Clear();
            return Convert.ToBase64String(keyArray);
        }
    }

    public static string CreateRNGKey(int keySize = 32)
    {
        byte[] keyArray = new byte[keySize];
        using (var provider = new RNGCryptoServiceProvider())
        {
            provider.GetNonZeroBytes(keyArray);
        }
        return Convert.ToBase64String(keyArray);
    }
    #endregion
}