0
votes

Custom allocation supports setting the initial device twin as well as the IoT Hub. The device twin does not get set.

I've configured the device provisioning service in Azure to use a custom Azure Function. In the Azure function, via custom API, we run logic to determine the best IoT Hub location for the device. In addition to the IoT Hub assignment, the code supplies the initial device twin data but It's not working.

I'm following the code sample described in this blog: https://sandervandevelde.wordpress.com/2018/12/29/custom-iot-hub-assignment-in-device-provisioning-service/

I've looked through several related custom allocation issues but haven't found anyone using the custom azure function approach as described above.

Here is a snippet of the related code from the blog article above.

var response = new Response(savefileresponse.AssignedHub);
//loading an instance of the initiTwin didn't work, try typing device twin values in manually
//response.initialTwin = savefileresponse.initialTwin;
response.initialTwin.properties.desired = new JObject();
response.initialTwin.properties.desired.PropOne = "2345";
response.initialTwin.properties.desired.PropTwo = "6789";

Below are the class definitions taken from the Blog article. Note the use of the dynamic type for desired properties. Can someone confirm this is the correct message response type to DPS?

public class AssignDeviceResponse
{
    public AssignDeviceResponse()
    {
        this.initialTwin = new ResponseTwin();
    }

    public ProvisioningRegistrationStatusType Status { get; set; }
    public string DeviceId { get; set; }
    public string AssignedHub { get; set; }
    public ResponseTwin initialTwin { get; set; }
}

#endregion

#region Microsoft DPS response contracts
public class Response
{
    public Response(string hostName)
    {
        iotHubHostName = hostName;
        initialTwin = new ResponseTwin();
    }

    public string iotHubHostName { get; set; }
    public ResponseTwin initialTwin { get; set; }
}

public class ResponseTwin
{
    public ResponseTwin()
    {
        properties = new ResponseProperties();
    }

    public dynamic tags { get; set; }
    public ResponseProperties properties { get; set; } // contains desired properties
}

public class ResponseProperties
{
    public dynamic desired { get; set; }
}
#endregion

After provisioning, the IoT Hub value is returned to device correctly. Then I went into the azure portal, IoT Hub and show the device twin value. None of the custom properties I added show up. The twin below is apparently some IoT Hub default as it doesn't match the twin in the DPS service either.

{
"deviceId": "cde5d316-9c01-3961-b850-8f5c17cea937",
"etag": "AAAAAAAAAAE=",
"deviceEtag": "NzA1OTc5MzE1",
"status": "enabled",
"statusUpdateTime": "0001-01-01T00:00:00",
"connectionState": "Disconnected",
"lastActivityTime": "2019-04-26T16:41:11.6618195",
"cloudToDeviceMessageCount": 0,
"authenticationType": "selfSigned",
"x509Thumbprint": {
"primaryThumbprint": 
  "xxx",
"secondaryThumbprint": 
  "xxx"
},
"version": 2,
"properties": {
  "desired": {
    "$metadata": {
      "$lastUpdated": "2019-04-26T16:41:09.4381992Z"
   },
   "$version": 1
},
"reported": {
  "$metadata": {
    "$lastUpdated": "2019-04-26T16:41:09.4381992Z"
  },
  "$version": 1
 }
},
"capabilities": {
"iotEdge": false
}
}

How can I set the device twin default value via custom allocation?

EDIT: After trying a different response type on my function, I thought I'd also post the code that works for me to read the parameters as well as forming the response that does serialize correctly through DPS. The issue still remains how to set the desired initialTwin values. Here's the azure function code that serializes back to DPS.

public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
                                                      HttpRequestMessage req, TraceWriter log)
    {
        string requestBody = await req.Content.ReadAsStringAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);

        //Read out key information
        string deviceId = data.deviceRuntimeContext.registrationId;
        string certificate = data.deviceRuntimeContext.x509.clientCertificate;
        var response = new Response(data.linkedHubs?[0]);

        //Can't get initialTwin data back to DPS
        //response.initialTwin = new ResponseTwin() { }

        return req.CreateResponse<Response>(HttpStatusCode.OK, response);
    }
4

4 Answers

1
votes

From this statement, "None of the custom properties I added show up" in your question, I assume that the device was already provisioned and you were provisioning the device again. In this case DPS assumes that the customer's solution already has a provisioned state, we do not want to overwrite it. Depending on the scenario, you can take actions to satisfy your needs. If this is testing and you are not concerned about preexisting data related to this device, you can delete the device from the hub and do a re-provision. Now the new twin data will show up from the DPS.

0
votes

the following code snippet is an example of the bypass custom function in the individual enrollment:

#r "Newtonsoft.Json"

using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");
    //log.LogInformation($"\nHeaders:\n\t{string.Join("\n\t", req.Headers.Select(i => $"{i.Key}={i.Value.First()}"))}");

    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    string iotHub = data?.individualEnrollment?.iotHubs?[0];

    dynamic result = new { iotHubHostName = $"{iotHub}" };
    return (ActionResult)new OkObjectResult(result);
}

In the case of the initializing a device twin, we need to add an initialTwin property object like is shown in the following example :

dynamic result = new { 
    iotHubHostName = $"{iotHub}", 
    initialTwin = new { 
        tags = new { abcd = 12345}, 
        properties = new { 
            desired = new { 
                PropOne = "2345",
                PropTwo = "6789"
                }
            }
        }
    };
0
votes

I suspect this will help someone else, the code below works but just barely. i haven't been able to modify the dynamic result without getting a serialization error but at least this Azure Function code (not script), work. The key is the cast in the response.

   public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
                                                      HttpRequestMessage req, TraceWriter log)
    {
        log.Info("C# HTTP trigger function processed a request.");
        //log.LogInformation($"\nHeaders:\n\t{string.Join("\n\t", req.Headers.Select(i => $"{i.Key}={i.Value.First()}"))}");

        string requestBody = await req.Content.ReadAsStringAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        string iotHub = data.linkedHubs[0];

        dynamic result = new
        {
            iotHubHostName = $"{iotHub}",
            initialTwin = new
            {
                tags = new { abcd = 12345 },
                properties = new
                {
                    desired = new
                    {
                        PropOne = "2345",
                        PropTwo = "6789"
                    }
                }
            }
        };
        log.Info($"Sending back result: {Convert.ToString(result)}");

        return req.CreateResponse<dynamic>(HttpStatusCode.OK, (Object)result);
    }
0
votes

Ok, after working this through with Microsoft's help, here is the answer to the question.

1.) Implement the Azure Function in a V2 instance of Azure Functions

2.) Add reference to Microsoft.Azure.Devices.Shared

The DPS code is expecting an instance of TwinState as shown below

public class ResponseObj
{
    public string iotHubHostName { get; set; }
    public TwinState initialTwin { get; set; }
}

Then serialization is handled by this return type

return (ActionResult)new OkObjectResult(response);

The TwinState class in Java: https://github.com/Azure/azure-iot-sdk-java/blob/a9c1487bede6463a6d67aadd68588cdaf92fd905/deps/src/main/java/com/microsoft/azure/sdk/iot/deps/twin/TwinState.java#L123