Przewodnik dewelopera urządzeń IoT Plug and Play

Usługa IoT Plug and Play umożliwia tworzenie urządzeń IoT, które anonsują ich możliwości w aplikacjach usługi Azure IoT. Urządzenia IoT Plug and Play nie wymagają ręcznej konfiguracji, gdy klient łączy je z aplikacjami obsługującymi usługę IoT Plug and Play, takimi jak usługa IoT Central.

Urządzenie IoT można zaimplementować bezpośrednio przy użyciu modułów lub modułów usługi IoT Edge.

W tym przewodniku opisano podstawowe kroki wymagane do utworzenia urządzenia, modułu lub modułu usługi IoT Edge, który jest zgodny z konwencjami IoT Plug and Play.

Aby utworzyć urządzenie IoT Plug and Play, moduł lub moduł usługi IoT Edge, wykonaj następujące kroki:

  1. Upewnij się, że urządzenie używa protokołu MQTT lub MQTT za pośrednictwem protokołu WebSockets w celu nawiązania połączenia z usługą Azure IoT Hub.
  2. Utwórz model języka DTDL (Digital Twins Definition Language), aby opisać urządzenie. Aby dowiedzieć się więcej, zobacz Omówienie składników w modelach usługi IoT Plug and Play.
  3. Zaktualizuj urządzenie lub moduł, aby ogłosić model-id je w ramach połączenia urządzenia.
  4. Implementowanie danych telemetrycznych, właściwości i poleceń, które są zgodne z konwencjami IoT Plug and Play

Gdy wdrożenie urządzenia lub modułu będzie gotowe, użyj eksploratora usługi Azure IoT, aby sprawdzić, czy urządzenie jest zgodne z konwencjami IoT Plug and Play.

Anons identyfikatora modelu

Aby ogłosić identyfikator modelu, urządzenie musi je uwzględnić w informacjach o połączeniu:

static const char g_ThermostatModelId[] = "dtmi:com:example:Thermostat;1";
IOTHUB_DEVICE_CLIENT_LL_HANDLE deviceHandle = NULL;
deviceHandle = CreateDeviceClientLLHandle();
iothubResult = IoTHubDeviceClient_LL_SetOption(
    deviceHandle, OPTION_MODEL_ID, g_ThermostatModelId);

Napiwek

W przypadku modułów i usługi IoT Edge należy użyć zamiast IoTHubDeviceClient_LLelementu IoTHubModuleClient_LL .

Napiwek

Jest to jedyny czas, gdy urządzenie może ustawić identyfikator modelu, nie można go zaktualizować po nawiązaniu połączenia z urządzeniem.

Ładunek DPS

Urządzenia korzystające z usługi Device Provisioning Service (DPS) mogą zawierać modelId element do użycia podczas procesu aprowizacji przy użyciu następującego ładunku JSON:

{
    "modelId" : "dtmi:com:example:Thermostat;1"
}

Używanie składników

Zgodnie z opisem w artykule Opis składników w modelach usługi IoT Plug and Play należy zdecydować, czy chcesz używać składników do opisywania urządzeń. W przypadku używania składników urządzenia muszą postępować zgodnie z regułami opisanymi w poniższych sekcjach:

Telemetria

Składnik domyślny nie wymaga żadnej specjalnej właściwości dodanej do komunikatu telemetrii.

W przypadku używania składników zagnieżdżonych urządzenia muszą ustawić właściwość komunikatu o nazwie składnika:

void PnP_ThermostatComponent_SendTelemetry(
    PNP_THERMOSTAT_COMPONENT_HANDLE pnpThermostatComponentHandle,
    IOTHUB_DEVICE_CLIENT_LL_HANDLE deviceClientLL)
{
    PNP_THERMOSTAT_COMPONENT* pnpThermostatComponent = (PNP_THERMOSTAT_COMPONENT*)pnpThermostatComponentHandle;
    IOTHUB_MESSAGE_HANDLE messageHandle = NULL;
    IOTHUB_CLIENT_RESULT iothubResult;

    char temperatureStringBuffer[32];

    if (snprintf(
        temperatureStringBuffer,
        sizeof(temperatureStringBuffer),
        g_temperatureTelemetryBodyFormat,
        pnpThermostatComponent->currentTemperature) < 0)
    {
        LogError("snprintf of current temperature telemetry failed");
    }
    else if ((messageHandle = PnP_CreateTelemetryMessageHandle(
        pnpThermostatComponent->componentName, temperatureStringBuffer)) == NULL)
    {
        LogError("Unable to create telemetry message");
    }
    else if ((iothubResult = IoTHubDeviceClient_LL_SendEventAsync(
        deviceClientLL, messageHandle, NULL, NULL)) != IOTHUB_CLIENT_OK)
    {
        LogError("Unable to send telemetry message, error=%d", iothubResult);
    }

    IoTHubMessage_Destroy(messageHandle);
}

// ...

PnP_ThermostatComponent_SendTelemetry(g_thermostatHandle1, deviceClient);

Właściwości tylko do odczytu

Raportowanie właściwości z domyślnego składnika nie wymaga żadnej specjalnej konstrukcji:

static const char g_maxTemperatureSinceRebootFormat[] = "{\"maxTempSinceLastReboot\":%.2f}";

char maxTemperatureSinceRebootProperty[256];

snprintf(
    maxTemperatureSinceRebootProperty,
    sizeof(maxTemperatureSinceRebootProperty),
    g_maxTemperatureSinceRebootFormat,
    38.7);

IOTHUB_CLIENT_RESULT iothubClientResult = IoTHubDeviceClient_LL_SendReportedState(
    deviceClientLL,
    (const unsigned char*)maxTemperatureSinceRebootProperty,
    strlen(maxTemperatureSinceRebootProperty), NULL, NULL));

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "maxTempSinceLastReboot" : 38.7
  }
}

W przypadku używania składników zagnieżdżonych utwórz właściwości w nazwie składnika i dołącz znacznik:

STRING_HANDLE PnP_CreateReportedProperty(
    const char* componentName,
    const char* propertyName,
    const char* propertyValue
)
{
    STRING_HANDLE jsonToSend;

    if (componentName == NULL) 
    {
        jsonToSend = STRING_construct_sprintf(
            "{\"%s\":%s}",
            propertyName, propertyValue);
    }
    else 
    {
       jsonToSend = STRING_construct_sprintf(
            "{\"""%s\":{\"__t\":\"c\",\"%s\":%s}}",
            componentName, propertyName, propertyValue);
    }

    if (jsonToSend == NULL)
    {
        LogError("Unable to allocate JSON buffer");
    }

    return jsonToSend;
}

void PnP_TempControlComponent_Report_MaxTempSinceLastReboot_Property(
    PNP_THERMOSTAT_COMPONENT_HANDLE pnpThermostatComponentHandle,
    IOTHUB_DEVICE_CLIENT_LL_HANDLE deviceClientLL)
{
    PNP_THERMOSTAT_COMPONENT* pnpThermostatComponent =
        (PNP_THERMOSTAT_COMPONENT*)pnpThermostatComponentHandle;
    char maximumTemperatureAsString[32];
    IOTHUB_CLIENT_RESULT iothubClientResult;
    STRING_HANDLE jsonToSend = NULL;

    if (snprintf(maximumTemperatureAsString, sizeof(maximumTemperatureAsString),
        "%.2f", pnpThermostatComponent->maxTemperature) < 0)
    {
        LogError("Unable to create max temp since last reboot string for reporting result");
    }
    else if ((jsonToSend = PnP_CreateReportedProperty(
                pnpThermostatComponent->componentName,
                g_maxTempSinceLastRebootPropertyName,
                maximumTemperatureAsString)) == NULL)
    {
        LogError("Unable to build max temp since last reboot property");
    }
    else
    {
        const char* jsonToSendStr = STRING_c_str(jsonToSend);
        size_t jsonToSendStrLen = strlen(jsonToSendStr);

        if ((iothubClientResult = IoTHubDeviceClient_LL_SendReportedState(
                deviceClientLL,
                (const unsigned char*)jsonToSendStr,
                jsonToSendStrLen, NULL, NULL)) != IOTHUB_CLIENT_OK)
        {
            LogError("Unable to send reported state, error=%d", iothubClientResult);
        }
        else
        {
            LogInfo("Sending maximumTemperatureSinceLastReboot property to IoTHub for component=%s",
                pnpThermostatComponent->componentName);
        }
    }

    STRING_delete(jsonToSend);
}

// ...

PnP_TempControlComponent_Report_MaxTempSinceLastReboot_Property(g_thermostatHandle1, deviceClient);

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1" : {  
      "__t" : "c",  
      "maxTemperature" : 38.7
     }
  }
}

Właściwości z możliwością zapisu

Te właściwości można ustawić na urządzeniu lub zaktualizować przez aplikację zaplecza. Jeśli aplikacja zaplecza aktualizuje właściwość, klient otrzymuje powiadomienie jako wywołanie zwrotne w elemecie DeviceClient lub ModuleClient. Aby postępować zgodnie z konwencjami IoT Plug and Play, urządzenie musi poinformować usługę, że właściwość została pomyślnie odebrana.

Jeśli typ właściwości to Object, usługa musi wysłać pełny obiekt do urządzenia, nawet jeśli aktualizuje tylko podzestaw pól obiektu. Potwierdzenie wysyłane przez urządzenie może być również kompletnym obiektem.

Zgłaszanie właściwości możliwej do zapisu

Gdy urządzenie zgłasza właściwość zapisywalną, musi zawierać ack wartości zdefiniowane w konwencjach.

Aby zgłosić właściwość zapisywalną z domyślnego składnika:

IOTHUB_CLIENT_RESULT iothubClientResult;
char targetTemperatureResponseProperty[256];

snprintf(
    targetTemperatureResponseProperty,
    sizeof(targetTemperatureResponseProperty),
    "{\"targetTemperature\":{\"value\":%.2f,\"ac\":%d,\"av\":%d,\"ad\":\"%s\"}}",
    23.2, 200, 3, "Successfully updated target temperature");

iothubClientResult = IoTHubDeviceClient_LL_SendReportedState(
    deviceClientLL,
    (const unsigned char*)targetTemperatureResponseProperty,
    strlen(targetTemperatureResponseProperty), NULL, NULL);

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "Successfully updated target temperature"
      }
  }
}

Aby zgłosić właściwość zapisywalną ze składnika zagnieżdżonego, reprezentacja bliźniacze musi zawierać znacznik:

STRING_HANDLE PnP_CreateReportedPropertyWithStatus(const char* componentName,
    const char* propertyName, const char* propertyValue,
    int result, const char* description, int ackVersion
)
{
    STRING_HANDLE jsonToSend;

    if (componentName == NULL) 
    {
        jsonToSend = STRING_construct_sprintf(
            "{\"%s\":{\"value\":%s,\"ac\":%d,\"ad\":\"%s\",\"av\":%d}}",
            propertyName, propertyValue,
            result, description, ackVersion);
    }
    else
    {
       jsonToSend = STRING_construct_sprintf(
            "{\"""%s\":{\"__t\":\"c\",\"%s\":{\"value\":%s,\"ac\":%d,\"ad\":\"%s\",\"av\":%d}}}",
            componentName, propertyName, propertyValue,
            result, description, ackVersion);
    }

    if (jsonToSend == NULL)
    {
        LogError("Unable to allocate JSON buffer");
    }

    return jsonToSend;
}

// ...

char targetTemperatureAsString[32];
IOTHUB_CLIENT_RESULT iothubClientResult;
STRING_HANDLE jsonToSend = NULL;

snprintf(targetTemperatureAsString,
    sizeof(targetTemperatureAsString),
    "%.2f",
    23.2);
jsonToSend = PnP_CreateReportedPropertyWithStatus(
    "thermostat1",
    "targetTemperature",
    targetTemperatureAsString,
    200,
    "complete",
    3);

const char* jsonToSendStr = STRING_c_str(jsonToSend);
size_t jsonToSendStrLen = strlen(jsonToSendStr);

iothubClientResult = IoTHubDeviceClient_LL_SendReportedState(
    deviceClientLL,
    (const unsigned char*)jsonToSendStr,
    jsonToSendStrLen, NULL, NULL);

STRING_delete(jsonToSend);

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1": {
      "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
    }
  }
}

Subskrybowanie żądanych aktualizacji właściwości

Usługi mogą aktualizować żądane właściwości, które wyzwalają powiadomienie na połączonych urządzeniach. To powiadomienie zawiera zaktualizowane żądane właściwości, w tym numer wersji identyfikujący aktualizację. Urządzenia muszą zawierać ten numer wersji w ack komunikacie wysłanym z powrotem do usługi.

Składnik domyślny widzi pojedynczą właściwość i tworzy raport ack z odebraną wersją:

static void Thermostat_DeviceTwinCallback(
    DEVICE_TWIN_UPDATE_STATE updateState,
    const unsigned char* payload,
    size_t size,
    void* userContextCallback)
{
    // The device handle associated with this request is passed as the context,
    // since we will need to send reported events back.
    IOTHUB_DEVICE_CLIENT_LL_HANDLE deviceClientLL =
        (IOTHUB_DEVICE_CLIENT_LL_HANDLE)userContextCallback;

    char* jsonStr = NULL;
    JSON_Value* rootValue = NULL;
    JSON_Object* desiredObject;
    JSON_Value* versionValue = NULL;
    JSON_Value* targetTemperatureValue = NULL;

    jsonStr = CopyTwinPayloadToString(payload, size));
    rootValue = json_parse_string(jsonStr));
    desiredObject = GetDesiredJson(updateState, rootValue));
    targetTemperatureValue = json_object_get_value(desiredObject, "targetTemperature"));
    versionValue = json_object_get_value(desiredObject, "$version"));
    json_value_get_type(versionValue);
    json_value_get_type(targetTemperatureValue);

    double targetTemperature = json_value_get_number(targetTemperatureValue);
    int version = (int)json_value_get_number(versionValue);

    // ...

    // The device needs to let the service know that it has received the targetTemperature desired property.
    SendTargetTemperatureReport(deviceClientLL, targetTemperature, 200, version, "Successfully updated target temperature");

    json_value_free(rootValue);
    free(jsonStr);
}

// ...

IOTHUB_CLIENT_RESULT iothubResult;
iothubResult = IoTHubDeviceClient_LL_SetDeviceTwinCallback(
    deviceHandle, Thermostat_DeviceTwinCallback, (void*)deviceHandle))

Bliźniacze reprezentacje urządzenia dla zagnieżdżonego składnika przedstawiają żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "targetTemperature": 23.2,
    "$version" : 3
  },
  "reported": {
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "Successfully updated target temperature"
      }
  }
}

Zagnieżdżony składnik odbiera żądane właściwości opakowane nazwą składnika i powinien zgłosić raport z powrotem zgłoszoną ack właściwość:

bool PnP_ProcessTwinData(
    DEVICE_TWIN_UPDATE_STATE updateState,
    const unsigned char* payload,
    size_t size, const char** componentsInModel,
    size_t numComponentsInModel,
    PnP_PropertyCallbackFunction pnpPropertyCallback,
    void* userContextCallback)
{
    char* jsonStr = NULL;
    JSON_Value* rootValue = NULL;
    JSON_Object* desiredObject;
    bool result;

    jsonStr = PnP_CopyPayloadToString(payload, size));
    rootValue = json_parse_string(jsonStr));
    desiredObject = GetDesiredJson(updateState, rootValue));
    
    result = VisitDesiredObject(
        desiredObject, componentsInModel,
        numComponentsInModel, pnpPropertyCallback,
        userContextCallback);


    json_value_free(rootValue);
    free(jsonStr);

    return result;
}

// ...
static const char g_thermostatComponent1Name[] = "thermostat1";
static const size_t g_thermostatComponent1Size = sizeof(g_thermostatComponent1Name) - 1;
static const char g_thermostatComponent2Name[] = "thermostat2";

static const char* g_modeledComponents[] = {g_thermostatComponent1Name, g_thermostatComponent2Name};
static const size_t g_numModeledComponents = sizeof(g_modeledComponents) / sizeof(g_modeledComponents[0]);

static void PnP_TempControlComponent_DeviceTwinCallback(
    DEVICE_TWIN_UPDATE_STATE updateState,
    const unsigned char* payload,
    size_t size,
    void* userContextCallback
)
{
    PnP_ProcessTwinData(
        updateState, payload,
        size, g_modeledComponents,
        g_numModeledComponents,
        PnP_TempControlComponent_ApplicationPropertyCallback,
        userContextCallback);
}

Bliźniacze reprezentacje urządzenia dla zagnieżdżonego składnika przedstawiają żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "thermostat1" : {
        "__t" : "c",
        "targetTemperature": 23.2,
    }
    "$version" : 3
  },
  "reported": {
    "thermostat1" : {
        "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
    }
  }
}

Polecenia

Domyślny składnik otrzymuje nazwę polecenia, która została wywołana przez usługę.

Zagnieżdżony składnik otrzymuje nazwę polecenia poprzedzoną nazwą składnika i separatorem * .

void PnP_ParseCommandName(
    const char* deviceMethodName,
    unsigned const char** componentName,
    size_t* componentNameSize,
    const char** pnpCommandName
)
{
    const char* separator;

    if ((separator = strchr(deviceMethodName, "*")) != NULL)
    {
        *componentName = (unsigned const char*)deviceMethodName;
        *componentNameSize = separator - deviceMethodName;
        *pnpCommandName = separator + 1;
    }
    else
    {
        *componentName = NULL;
        *componentNameSize = 0;
        *pnpCommandName = deviceMethodName;
    }
}

static int PnP_TempControlComponent_DeviceMethodCallback(
    const char* methodName,
    const unsigned char* payload,
    size_t size,
    unsigned char** response,
    size_t* responseSize,
    void* userContextCallback)
{
    (void)userContextCallback;

    char* jsonStr = NULL;
    JSON_Value* rootValue = NULL;
    int result;
    unsigned const char *componentName;
    size_t componentNameSize;
    const char *pnpCommandName;

    *response = NULL;
    *responseSize = 0;

    // Parse the methodName into its componentName and CommandName.
    PnP_ParseCommandName(methodName, &componentName, &componentNameSize, &pnpCommandName);

    // Parse the JSON of the payload request.
    jsonStr = PnP_CopyPayloadToString(payload, size));
    rootValue = json_parse_string(jsonStr));
    if (componentName != NULL)
    {
        if (strncmp((const char*)componentName, g_thermostatComponent1Name, g_thermostatComponent1Size) == 0)
        {
            result = PnP_ThermostatComponent_ProcessCommand(g_thermostatHandle1, pnpCommandName, rootValue, response, responseSize);
        }
        else if (strncmp((const char*)componentName, g_thermostatComponent2Name, g_thermostatComponent2Size) == 0)
        {
            result = PnP_ThermostatComponent_ProcessCommand(g_thermostatHandle2, pnpCommandName, rootValue, response, responseSize);
        }
        else
        {
            LogError("PnP component=%.*s is not supported by TemperatureController", (int)componentNameSize, componentName);
            result = PNP_STATUS_NOT_FOUND;
        }
    }
    else
    {
        LogInfo("Received PnP command for TemperatureController component, command=%s", pnpCommandName);
        if (strcmp(pnpCommandName, g_rebootCommand) == 0)
        {
            result = PnP_TempControlComponent_InvokeRebootCommand(rootValue);
        }
        else
        {
            LogError("PnP command=s%s is not supported by TemperatureController", pnpCommandName);
            result = PNP_STATUS_NOT_FOUND;
        }
    }

    if (*response == NULL)
    {
        SetEmptyCommandResponse(response, responseSize, &result);
    }

    json_value_free(rootValue);
    free(jsonStr);

    return result;
}

// ...

PNP_DEVICE_CONFIGURATION g_pnpDeviceConfiguration;
g_pnpDeviceConfiguration.deviceMethodCallback = PnP_TempControlComponent_DeviceMethodCallback;
deviceClient = PnP_CreateDeviceClientLLHandle(&g_pnpDeviceConfiguration);

Ładunki żądań i odpowiedzi

Polecenia używają typów do definiowania ładunków żądań i odpowiedzi. Urządzenie musi deserializować przychodzący parametr wejściowy i serializować odpowiedź.

W poniższym przykładzie pokazano, jak zaimplementować polecenie ze złożonymi typami zdefiniowanymi w ładunkach:

{
  "@type": "Command",
  "name": "getMaxMinReport",
  "displayName": "Get Max-Min report.",
  "description": "This command returns the max, min and average temperature from the specified time to the current time.",
  "request": {
    "name": "since",
    "displayName": "Since",
    "description": "Period to return the max-min report.",
    "schema": "dateTime"
  },
  "response": {
    "name" : "tempReport",
    "displayName": "Temperature Report",
    "schema": {
      "@type": "Object",
      "fields": [
        {
          "name": "maxTemp",
          "displayName": "Max temperature",
          "schema": "double"
        },
        {
          "name": "minTemp",
          "displayName": "Min temperature",
          "schema": "double"
        },
        {
          "name" : "avgTemp",
          "displayName": "Average Temperature",
          "schema": "double"
        },
        {
          "name" : "startTime",
          "displayName": "Start Time",
          "schema": "dateTime"
        },
        {
          "name" : "endTime",
          "displayName": "End Time",
          "schema": "dateTime"
        }
      ]
    }
  }
}

Poniższe fragmenty kodu pokazują, jak urządzenie implementuje tę definicję polecenia, w tym typy używane do serializacji i deserializacji:

static const char g_maxMinCommandResponseFormat[] = "{\"maxTemp\":%.2f,\"minTemp\":%.2f,\"avgTemp\":%.2f,\"startTime\":\"%s\",\"endTime\":\"%s\"}";

// ...

static bool BuildMaxMinCommandResponse(
    PNP_THERMOSTAT_COMPONENT* pnpThermostatComponent,
    unsigned char** response,
    size_t* responseSize)
{
    int responseBuilderSize = 0;
    unsigned char* responseBuilder = NULL;
    bool result;
    char currentTime[TIME_BUFFER_SIZE];

    BuildUtcTimeFromCurrentTime(currentTime, sizeof(currentTime));
    responseBuilderSize = snprintf(NULL, 0, g_maxMinCommandResponseFormat,
        pnpThermostatComponent->maxTemperature,
        pnpThermostatComponent->minTemperature,
        pnpThermostatComponent->allTemperatures /
        pnpThermostatComponent->numTemperatureUpdates,
        g_programStartTime, currentTime));

    responseBuilder = calloc(1, responseBuilderSize + 1));

    responseBuilderSize = snprintf(
        (char*)responseBuilder, responseBuilderSize + 1, g_maxMinCommandResponseFormat,
        pnpThermostatComponent->maxTemperature,
        pnpThermostatComponent->minTemperature,
        pnpThermostatComponent->allTemperatures / pnpThermostatComponent->numTemperatureUpdates,
        g_programStartTime,
        currentTime));

    *response = responseBuilder;
    *responseSize = (size_t)responseBuilderSize;

    return true;
}

Napiwek

Nazwy żądań i odpowiedzi nie są obecne w serializowanych ładunkach przesyłanych za pośrednictwem przewodu.

Zestawy SDK

Fragmenty kodu w tym artykule są oparte na przykładach korzystających z dodatku oprogramowania pośredniczącego usługi Azure IoT dla środowiska Eclipse ThreadX. Dodatek to warstwa powiązania między środowiskiem Eclipse ThreadX i zestawem Azure SDK dla osadzonego języka C.

Fragmenty kodu w tym artykule są oparte na następujących przykładach:

Anons identyfikatora modelu

Aby ogłosić identyfikator modelu, urządzenie musi je uwzględnić w informacjach o połączeniu:

#include "nx_azure_iot_hub_client.h"

// ...

#define SAMPLE_PNP_MODEL_ID "dtmi:com:example:Thermostat;1"

// ...

status = nx_azure_iot_hub_client_model_id_set(iothub_client_ptr, (UCHAR *)SAMPLE_PNP_MODEL_ID, sizeof(SAMPLE_PNP_MODEL_ID) - 1);

Napiwek

Jest to jedyny czas, gdy urządzenie może ustawić identyfikator modelu, nie można go zaktualizować po nawiązaniu połączenia z urządzeniem.

Ładunek DPS

Urządzenia korzystające z usługi Device Provisioning Service (DPS) mogą zawierać modelId wartość, która ma być używana podczas procesu aprowizacji przy użyciu następującego ładunku JSON:

{
    "modelId" : "dtmi:com:example:Thermostat;1"
}

W przykładzie użyto następującego kodu do wysłania tego ładunku:

#include "nx_azure_iot_provisioning_client.h"

// ...

#define SAMPLE_PNP_MODEL_ID "dtmi:com:example:Thermostat;1"
#define SAMPLE_PNP_DPS_PAYLOAD "{\"modelId\":\"" SAMPLE_PNP_MODEL_ID "\"}"

// ...

status = nx_azure_iot_provisioning_client_registration_payload_set(prov_client_ptr, (UCHAR *)SAMPLE_PNP_DPS_PAYLOAD, sizeof(SAMPLE_PNP_DPS_PAYLOAD) - 1);

Używanie składników

Zgodnie z opisem w artykule Opis składników w modelach usługi IoT Plug and Play należy zdecydować, czy chcesz używać składników do opisywania urządzeń. W przypadku używania składników urządzenia muszą przestrzegać reguł opisanych w poniższych sekcjach. Aby uprościć pracę z konwencjami IoT Plug and Play dla składników, przykłady korzystają z funkcji pomocnika w nx_azure_iot_hub_client.h.

Telemetria

Składnik domyślny nie wymaga żadnej specjalnej właściwości dodanej do komunikatu telemetrii.

W przypadku używania składników zagnieżdżonych urządzenia muszą ustawić właściwość komunikatu z nazwą składnika. W poniższym fragmencie component_name_ptr kodu jest nazwą składnika, takiego jak thermostat1. Funkcja nx_azure_iot_pnp_helper_telemetry_message_create pomocnika zdefiniowana w pliku nx_azure_iot_pnp_helpers.h dodaje właściwość komunikatu o nazwie składnika:

#include "nx_azure_iot_pnp_helpers.h"

// ...

static const CHAR telemetry_name[] = "temperature";

// ...

UINT sample_pnp_thermostat_telemetry_send(SAMPLE_PNP_THERMOSTAT_COMPONENT *handle, NX_AZURE_IOT_HUB_CLIENT *iothub_client_ptr)
{
UINT status;
NX_PACKET *packet_ptr;
NX_AZURE_IOT_JSON_WRITER json_writer;
UINT buffer_length;

    // ...

    /* Create a telemetry message packet. */
    if ((status = nx_azure_iot_pnp_helper_telemetry_message_create(iothub_client_ptr, handle -> component_name_ptr,
        handle -> component_name_length,
        &packet_ptr, NX_WAIT_FOREVER)))
    {
        // ...
    }

    // ...

    if ((status = nx_azure_iot_hub_client_telemetry_send(iothub_client_ptr, packet_ptr,
        (UCHAR *)scratch_buffer, buffer_length, NX_WAIT_FOREVER)))
    {
        // ...
    }

    // ...

    return(status);
}

Właściwości tylko do odczytu

Raportowanie właściwości z domyślnego składnika nie wymaga żadnej specjalnej konstrukcji:

#include "nx_azure_iot_hub_client.h"
#include "nx_azure_iot_json_writer.h"

// ...

static const CHAR reported_max_temp_since_last_reboot[] = "maxTempSinceLastReboot";

// ...

static UINT sample_build_reported_property(NX_AZURE_IOT_JSON_WRITER *json_builder_ptr, double temp)
{
UINT ret;

    if (nx_azure_iot_json_writer_append_begin_object(json_builder_ptr) ||
        nx_azure_iot_json_writer_append_property_with_double_value(json_builder_ptr,
            (UCHAR *)reported_max_temp_since_last_reboot,
            sizeof(reported_max_temp_since_last_reboot) - 1,
            temp, DOUBLE_DECIMAL_PLACE_DIGITS) ||
        nx_azure_iot_json_writer_append_end_object(json_builder_ptr))
    {
        ret = 1;
        printf("Failed to build reported property\r\n");
    }
    else
    {
        ret = 0;
    }

    return(ret);
}

// ...

if ((status = sample_build_reported_property(&json_builder, device_max_temp)))
{
    // ...
}

reported_properties_length = nx_azure_iot_json_writer_get_bytes_used(&json_builder);
if ((status = nx_azure_iot_hub_client_device_twin_reported_properties_send(&(context -> iothub_client),
    scratch_buffer,
    reported_properties_length,
    &request_id, &response_status,
    &reported_property_version,
    (5 * NX_IP_PERIODIC_RATE))))
{
    // ...
}

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "maxTempSinceLastReboot" : 38.7
  }
}

W przypadku używania składników zagnieżdżonych właściwości należy utworzyć w nazwie składnika i dołączyć znacznik. W poniższym fragmencie component_name_ptr kodu jest nazwą składnika, takiego jak thermostat1. Funkcja nx_azure_iot_pnp_helper_build_reported_property pomocnika zdefiniowana w pliku nx_azure_iot_pnp_helpers.h tworzy zgłoszoną właściwość w poprawnym formacie:

#include "nx_azure_iot_pnp_helpers.h"

// ...

static const CHAR reported_max_temp_since_last_reboot[] = "maxTempSinceLastReboot";

UINT sample_pnp_thermostat_report_max_temp_since_last_reboot_property(SAMPLE_PNP_THERMOSTAT_COMPONENT *handle, NX_AZURE_IOT_HUB_CLIENT *iothub_client_ptr)
{
UINT reported_properties_length;
UINT status;
UINT response_status;
UINT request_id;
NX_AZURE_IOT_JSON_WRITER json_builder;
ULONG reported_property_version;

    // ...

    if ((status = nx_azure_iot_pnp_helper_build_reported_property(handle -> component_name_ptr,
        handle -> component_name_length,
        append_max_temp, (VOID *)handle,
        &json_builder)))
    {
        // ...
    }

    reported_properties_length = nx_azure_iot_json_writer_get_bytes_used(&json_builder);
    if ((status = nx_azure_iot_hub_client_device_twin_reported_properties_send(iothub_client_ptr,
        scratch_buffer,
        reported_properties_length,
        &request_id, &response_status,
        &reported_property_version,
        (5 * NX_IP_PERIODIC_RATE))))
    {
        // ...
    }

    // ...
}

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
    "reported": {
        "thermostat1" : {  
            "__t" : "c",  
            "maxTemperature" : 38.7
        } 
    }
}

Właściwości z możliwością zapisu

Te właściwości można ustawić na urządzeniu lub zaktualizować przez aplikację zaplecza. Aby postępować zgodnie z konwencjami IoT Plug and Play, urządzenie musi poinformować usługę, że właściwość została pomyślnie odebrana.

Zgłaszanie właściwości możliwej do zapisu

Gdy urządzenie zgłasza właściwość zapisywalną, musi zawierać ack wartości zdefiniowane w konwencjach.

Aby zgłosić właściwość zapisywalną z domyślnego składnika:

#include "nx_azure_iot_hub_client.h"
#include "nx_azure_iot_json_writer.h"

// ...

static const CHAR reported_temp_property_name[] = "targetTemperature";
static const CHAR reported_value_property_name[] = "value";
static const CHAR reported_status_property_name[] = "ac";
static const CHAR reported_version_property_name[] = "av";
static const CHAR reported_description_property_name[] = "ad";

// ...

static VOID sample_send_target_temperature_report(SAMPLE_CONTEXT *context, double current_device_temp_value,
    UINT status, UINT version, UCHAR *description_ptr,
    UINT description_len)
{
NX_AZURE_IOT_JSON_WRITER json_builder;
UINT bytes_copied;
UINT response_status;
UINT request_id;
ULONG reported_property_version;

    // ...

    if (nx_azure_iot_json_writer_append_begin_object(&json_builder) ||
        nx_azure_iot_json_writer_append_property_name(&json_builder,
            (UCHAR *)reported_temp_property_name,
            sizeof(reported_temp_property_name) - 1) ||
        nx_azure_iot_json_writer_append_begin_object(&json_builder) ||
        nx_azure_iot_json_writer_append_property_with_double_value(&json_builder,
            (UCHAR *)reported_value_property_name,
            sizeof(reported_value_property_name) - 1,
            current_device_temp_value, DOUBLE_DECIMAL_PLACE_DIGITS) ||
        nx_azure_iot_json_writer_append_property_with_int32_value(&json_builder,
            (UCHAR *)reported_status_property_name,
            sizeof(reported_status_property_name) - 1,
            (int32_t)status) ||
        nx_azure_iot_json_writer_append_property_with_int32_value(&json_builder,
            (UCHAR *)reported_version_property_name,
            sizeof(reported_version_property_name) - 1,
            (int32_t)version) ||
        nx_azure_iot_json_writer_append_property_with_string_value(&json_builder,
            (UCHAR *)reported_description_property_name,
            sizeof(reported_description_property_name) - 1,
            description_ptr, description_len) ||
        nx_azure_iot_json_writer_append_end_object(&json_builder) ||
        nx_azure_iot_json_writer_append_end_object(&json_builder))
    {
        // ...
    }
    else
    // ...
}

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "success"
      }
  }
}

Aby zgłosić właściwość zapisywalną ze składnika zagnieżdżonego, reprezentacja bliźniacze musi zawierać znacznik, a właściwości muszą zostać utworzone w nazwie składnika. W poniższym fragmencie component_name_ptr kodu jest nazwą składnika, takiego jak thermostat1. Funkcja nx_azure_iot_pnp_helper_build_reported_property_with_status pomocnika zdefiniowana w pliku nx_azure_iot_pnp_helpers.h tworzy zgłoszony ładunek właściwości:

#include "nx_azure_iot_pnp_helpers.h"

// ...

static VOID sample_send_target_temperature_report(SAMPLE_PNP_THERMOSTAT_COMPONENT *handle,
    NX_AZURE_IOT_HUB_CLIENT *iothub_client_ptr, double temp,
    INT status_code, UINT version, const CHAR *description)
{
UINT bytes_copied;
UINT response_status;
UINT request_id;
NX_AZURE_IOT_JSON_WRITER json_writer;
ULONG reported_property_version;

    // ...

    if (nx_azure_iot_pnp_helper_build_reported_property_with_status(handle -> component_name_ptr, handle -> component_name_length,
        (UCHAR *)target_temp_property_name,
        sizeof(target_temp_property_name) - 1,
        append_temp, (VOID *)&temp, status_code,
        (UCHAR *)description,
        strlen(description), version, &json_writer))
    {
        // ...
    }
    else
    {
        // ...
    }

    // ...
}

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1": {
      "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "success"
      }
    }
  }
}

Subskrybowanie żądanych aktualizacji właściwości

Usługi mogą aktualizować żądane właściwości, które wyzwalają powiadomienie na połączonych urządzeniach. To powiadomienie zawiera zaktualizowane żądane właściwości i numer wersji identyfikujący aktualizację. Urządzenia muszą zawierać ten numer wersji w ack komunikacie wysłanym z powrotem do usługi.

Składnik domyślny widzi pojedynczą właściwość i tworzy raport ack z odebraną wersją:

#include "nx_azure_iot_hub_client.h"
#include "nx_azure_iot_json_writer.h"

// ...

static const CHAR temp_response_description[] = "success";

// ...

static UINT sample_parse_desired_temp_property(SAMPLE_CONTEXT *context,
    NX_AZURE_IOT_JSON_READER *json_reader_ptr,
    UINT is_partial)
{
double parsed_value;
UINT version;
NX_AZURE_IOT_JSON_READER copy_json_reader;
UINT status;

    // ...

    copy_json_reader = *json_reader_ptr;
    if (sample_json_child_token_move(&copy_json_reader,
            (UCHAR *)desired_version_property_name,
            sizeof(desired_version_property_name) - 1) ||
        nx_azure_iot_json_reader_token_int32_get(&copy_json_reader, (int32_t *)&version))
    {
        // ...
    }

    // ...

    sample_send_target_temperature_report(context, current_device_temp, 200,
        (UINT)version, (UCHAR *)temp_response_description,
        sizeof(temp_response_description) - 1);

    // ...
}

Zagnieżdżony składnik odbiera żądane właściwości opakowane nazwą składnika i tworzy raport ack z odebraną wersją:

#include "nx_azure_iot_pnp_helpers.h"

// ...

static const CHAR target_temp_property_name[] = "targetTemperature";
static const CHAR temp_response_description_success[] = "success";
static const CHAR temp_response_description_failed[] = "failed";

// ...

UINT sample_pnp_thermostat_process_property_update(SAMPLE_PNP_THERMOSTAT_COMPONENT *handle,
    NX_AZURE_IOT_HUB_CLIENT *iothub_client_ptr,
    UCHAR *component_name_ptr, UINT component_name_length,
    UCHAR *property_name_ptr, UINT property_name_length,
    NX_AZURE_IOT_JSON_READER *property_value_reader_ptr, UINT version)
{
double parsed_value = 0;
INT status_code;
const CHAR *description;

    // ...

    if (property_name_length != (sizeof(target_temp_property_name) - 1) ||
        strncmp((CHAR *)property_name_ptr, (CHAR *)target_temp_property_name, property_name_length) != 0)
    {
        // ...
    }
    else if (nx_azure_iot_json_reader_token_double_get(property_value_reader_ptr, &parsed_value))
    {
        status_code = 401;
        description = temp_response_description_failed;
    }
    else
    {
        status_code = 200;
        description = temp_response_description_success;

        // ...
    }

    sample_send_target_temperature_report(handle, iothub_client_ptr, parsed_value,
                                          status_code, version, description);

    // ...
}

Bliźniacze reprezentacje urządzenia dla zagnieżdżonego składnika przedstawiają żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "thermostat1" : {
        "__t" : "c",
        "targetTemperature": 23.2,
    }
    "$version" : 3
  },
  "reported": {
    "thermostat1" : {
        "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "success"
      }
    }
  }
}

Polecenia

Domyślny składnik otrzymuje nazwę polecenia, która została wywołana przez usługę.

Zagnieżdżony składnik otrzymuje nazwę polecenia poprzedzoną nazwą składnika i separatorem * . W poniższym fragmencie kodu funkcja nx_azure_iot_pnp_helper_command_name_parse pomocnika zdefiniowana w pliku nx_azure_iot_pnp_helpers.h analizuje nazwę składnika i nazwę polecenia z komunikatu odbieranego przez urządzenie z usługi:

#include "nx_azure_iot_hub_client.h"
#include "nx_azure_iot_pnp_helpers.h"

// ...

static VOID sample_direct_method_action(SAMPLE_CONTEXT *sample_context_ptr)
{
NX_PACKET *packet_ptr;
UINT status;
USHORT method_name_length;
const UCHAR *method_name_ptr;
USHORT context_length;
VOID *context_ptr;
UINT component_name_length;
const UCHAR *component_name_ptr;
UINT pnp_command_name_length;
const UCHAR *pnp_command_name_ptr;
NX_AZURE_IOT_JSON_WRITER json_writer;
NX_AZURE_IOT_JSON_READER json_reader;
NX_AZURE_IOT_JSON_READER *json_reader_ptr;
UINT status_code;
UINT response_length;

    // ...

    if ((status = nx_azure_iot_hub_client_direct_method_message_receive(&(sample_context_ptr -> iothub_client),
        &method_name_ptr, &method_name_length,
        &context_ptr, &context_length,
        &packet_ptr, NX_WAIT_FOREVER)))
    {
        // ...
    }

    // ...

    if ((status = nx_azure_iot_pnp_helper_command_name_parse(method_name_ptr, method_name_length,
        &component_name_ptr, &component_name_length,
        &pnp_command_name_ptr,
        &pnp_command_name_length)) != NX_AZURE_IOT_SUCCESS)
    {
        // ...
    }
    
    // ...

    else
    {
        // ...

        if ((status = sample_pnp_thermostat_process_command(&sample_thermostat_1, component_name_ptr,
            component_name_length, pnp_command_name_ptr,
            pnp_command_name_length, json_reader_ptr,
            &json_writer, &status_code)) == NX_AZURE_IOT_SUCCESS)
        {
            // ...
        }
        else if ((status = sample_pnp_thermostat_process_command(&sample_thermostat_2, component_name_ptr,
            component_name_length, pnp_command_name_ptr,
            pnp_command_name_length, json_reader_ptr,
            &json_writer, &status_code)) == NX_AZURE_IOT_SUCCESS)
        {
            // ...
        }
        else if((status = sample_pnp_temp_controller_process_command(component_name_ptr, component_name_length,
            pnp_command_name_ptr, pnp_command_name_length,
            json_reader_ptr, &json_writer,
            &status_code)) == NX_AZURE_IOT_SUCCESS)
        {
            // ...
        }
        else
        {
            printf("Failed to find any handler for method %.*s\r\n", method_name_length, method_name_ptr);
            status_code = SAMPLE_COMMAND_NOT_FOUND_STATUS;
            response_length = 0;
        }

        // ...
    }
}

Ładunki żądań i odpowiedzi

Polecenia używają typów do definiowania ładunków żądań i odpowiedzi. Urządzenie musi deserializować przychodzący parametr wejściowy i serializować odpowiedź.

W poniższym przykładzie pokazano, jak zaimplementować polecenie ze złożonymi typami zdefiniowanymi w ładunkach:

{
  "@type": "Command",
  "name": "getMaxMinReport",
  "displayName": "Get Max-Min report.",
  "description": "This command returns the max, min and average temperature from the specified time to the current time.",
  "request": {
    "name": "since",
    "displayName": "Since",
    "description": "Period to return the max-min report.",
    "schema": "dateTime"
  },
  "response": {
    "name" : "tempReport",
    "displayName": "Temperature Report",
    "schema": {
      "@type": "Object",
      "fields": [
        {
          "name": "maxTemp",
          "displayName": "Max temperature",
          "schema": "double"
        },
        {
          "name": "minTemp",
          "displayName": "Min temperature",
          "schema": "double"
        },
        {
          "name" : "avgTemp",
          "displayName": "Average Temperature",
          "schema": "double"
        },
        {
          "name" : "startTime",
          "displayName": "Start Time",
          "schema": "dateTime"
        },
        {
          "name" : "endTime",
          "displayName": "End Time",
          "schema": "dateTime"
        }
      ]
    }
  }
}

Poniższe fragmenty kodu pokazują, jak urządzenie implementuje tę definicję polecenia, w tym typy używane do serializacji i deserializacji:

#include "nx_azure_iot_pnp_helpers.h"

// ...

static const CHAR report_max_temp_name[] = "maxTemp";
static const CHAR report_min_temp_name[] = "minTemp";
static const CHAR report_avg_temp_name[] = "avgTemp";
static const CHAR report_start_time_name[] = "startTime";
static const CHAR report_end_time_name[] = "endTime";
static const CHAR fake_start_report_time[] = "2020-01-10T10:00:00Z";
static const CHAR fake_end_report_time[] = "2023-01-10T10:00:00Z";

// ...

static UINT sample_get_maxmin_report(SAMPLE_PNP_THERMOSTAT_COMPONENT *handle,
    NX_AZURE_IOT_JSON_READER *json_reader_ptr,
    NX_AZURE_IOT_JSON_WRITER *out_json_builder_ptr)
{
UINT status;
UCHAR *start_time = (UCHAR *)fake_start_report_time;
UINT start_time_len = sizeof(fake_start_report_time) - 1;
UCHAR time_buf[32];

    // ...

    /* Build the method response payload */
    if (nx_azure_iot_json_writer_append_begin_object(out_json_builder_ptr) ||
        nx_azure_iot_json_writer_append_property_with_double_value(out_json_builder_ptr,
            (UCHAR *)report_max_temp_name,
            sizeof(report_max_temp_name) - 1,
            handle -> maxTemperature,
            DOUBLE_DECIMAL_PLACE_DIGITS) ||
        nx_azure_iot_json_writer_append_property_with_double_value(out_json_builder_ptr,
            (UCHAR *)report_min_temp_name,
            sizeof(report_min_temp_name) - 1,
            handle -> minTemperature,
            DOUBLE_DECIMAL_PLACE_DIGITS) ||
        nx_azure_iot_json_writer_append_property_with_double_value(out_json_builder_ptr,
            (UCHAR *)report_avg_temp_name,
            sizeof(report_avg_temp_name) - 1,
            handle -> avgTemperature,
            DOUBLE_DECIMAL_PLACE_DIGITS) ||
        nx_azure_iot_json_writer_append_property_with_string_value(out_json_builder_ptr,
            (UCHAR *)report_start_time_name,
            sizeof(report_start_time_name) - 1,
            (UCHAR *)start_time, start_time_len) ||
        nx_azure_iot_json_writer_append_property_with_string_value(out_json_builder_ptr,
            (UCHAR *)report_end_time_name,
            sizeof(report_end_time_name) - 1,
            (UCHAR *)fake_end_report_time,
            sizeof(fake_end_report_time) - 1) ||
        nx_azure_iot_json_writer_append_end_object(out_json_builder_ptr))
    {
        status = NX_NOT_SUCCESSFUL;
    }
    else
    {
        status = NX_AZURE_IOT_SUCCESS;
    }

    return(status);
}

Napiwek

Nazwy żądań i odpowiedzi nie są obecne w serializowanych ładunkach przesyłanych za pośrednictwem przewodu.

Anons identyfikatora modelu

Aby ogłosić identyfikator modelu, urządzenie musi je uwzględnić w informacjach o połączeniu:

DeviceClient.CreateFromConnectionString(
  connectionString,
  TransportType.Mqtt,
  new ClientOptions() { ModelId = modelId })

Nowe ClientOptions przeciążenie jest dostępne we wszystkich DeviceClient metodach używanych do inicjowania połączenia.

Napiwek

W przypadku modułów i usługi IoT Edge należy użyć zamiast DeviceClientelementu ModuleClient .

Napiwek

Jest to jedyny czas, gdy urządzenie może ustawić identyfikator modelu, nie można go zaktualizować po nawiązaniu połączenia z urządzeniem.

Ładunek DPS

Urządzenia korzystające z usługi Device Provisioning Service (DPS) mogą zawierać modelId wartość, która ma być używana podczas procesu aprowizacji przy użyciu następującego ładunku JSON:

{
    "modelId" : "dtmi:com:example:Thermostat;1"
}

Używanie składników

Zgodnie z opisem w artykule Opis składników w modelach usługi IoT Plug and Play należy zdecydować, czy chcesz używać składników do opisywania urządzeń. W przypadku używania składników urządzenia muszą przestrzegać reguł opisanych w poniższych sekcjach.

Telemetria

Składnik domyślny nie wymaga żadnej specjalnej właściwości dodanej do komunikatu telemetrii.

W przypadku używania składników zagnieżdżonych urządzenia muszą ustawić właściwość komunikatu o nazwie składnika:

public async Task SendComponentTelemetryValueAsync(string componentName, string serializedTelemetry)
{
  var message = new Message(Encoding.UTF8.GetBytes(serializedTelemetry));
  message.ComponentName = componentName;
  message.ContentType = "application/json";
  message.ContentEncoding = "utf-8";
  await client.SendEventAsync(message);
}

Właściwości tylko do odczytu

Raportowanie właściwości z domyślnego składnika nie wymaga żadnej specjalnej konstrukcji:

TwinCollection reportedProperties = new TwinCollection();
reportedProperties["maxTemperature"] = 38.7;
await client.UpdateReportedPropertiesAsync(reportedProperties);

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "maxTemperature" : 38.7
  }
}

W przypadku używania składników zagnieżdżonych utwórz właściwości w nazwie składnika i dołącz znacznik:

TwinCollection reportedProperties = new TwinCollection();
TwinCollection component = new TwinCollection();
component["maxTemperature"] = 38.7;
component["__t"] = "c"; // marker to identify a component
reportedProperties["thermostat1"] = component;
await client.UpdateReportedPropertiesAsync(reportedProperties);

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1" : {  
      "__t" : "c",  
      "maxTemperature" : 38.7
     } 
  }
}

Właściwości z możliwością zapisu

Te właściwości można ustawić na urządzeniu lub zaktualizować przez aplikację zaplecza. Jeśli aplikacja zaplecza aktualizuje właściwość, klient otrzymuje powiadomienie jako wywołanie zwrotne w elemecie DeviceClient lub ModuleClient. Aby postępować zgodnie z konwencjami IoT Plug and Play, urządzenie musi poinformować usługę, że właściwość została pomyślnie odebrana.

Jeśli typ właściwości to Object, usługa musi wysłać pełny obiekt do urządzenia, nawet jeśli aktualizuje tylko podzestaw pól obiektu. Potwierdzenie wysyłane przez urządzenie musi być również kompletnym obiektem.

Zgłaszanie właściwości możliwej do zapisu

Gdy urządzenie zgłasza właściwość zapisywalną, musi zawierać ack wartości zdefiniowane w konwencjach.

Aby zgłosić właściwość zapisywalną z domyślnego składnika:

TwinCollection reportedProperties = new TwinCollection();
TwinCollection ackProps = new TwinCollection();
ackProps["value"] = 23.2;
ackProps["ac"] = 200; // using HTTP status codes
ackProps["av"] = 0; // not readed from a desired property
ackProps["ad"] = "reported default value";
reportedProperties["targetTemperature"] = ackProps;
await client.UpdateReportedPropertiesAsync(reportedProperties);

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
  }
}

Aby zgłosić właściwość zapisywalną ze składnika zagnieżdżonego, reprezentacja bliźniacze musi zawierać znacznik:

TwinCollection reportedProperties = new TwinCollection();
TwinCollection component = new TwinCollection();
TwinCollection ackProps = new TwinCollection();
component["__t"] = "c"; // marker to identify a component
ackProps["value"] = 23.2;
ackProps["ac"] = 200; // using HTTP status codes
ackProps["av"] = 0; // not read from a desired property
ackProps["ad"] = "reported default value";
component["targetTemperature"] = ackProps;
reportedProperties["thermostat1"] = component;
await client.UpdateReportedPropertiesAsync(reportedProperties);

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1": {
      "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
    }
  }
}

Subskrybowanie żądanych aktualizacji właściwości

Usługi mogą aktualizować żądane właściwości, które wyzwalają powiadomienie na połączonych urządzeniach. To powiadomienie zawiera zaktualizowane żądane właściwości, w tym numer wersji identyfikujący aktualizację. Urządzenia muszą zawierać ten numer wersji w ack komunikacie wysłanym z powrotem do usługi.

Składnik domyślny widzi pojedynczą właściwość i tworzy raport ack z odebraną wersją:

await client.SetDesiredPropertyUpdateCallbackAsync(async (desired, ctx) => 
{
  JValue targetTempJson = desired["targetTemperature"];
  double targetTemperature = targetTempJson.Value<double>();

  TwinCollection reportedProperties = new TwinCollection();
  TwinCollection ackProps = new TwinCollection();
  ackProps["value"] = targetTemperature;
  ackProps["ac"] = 200;
  ackProps["av"] = desired.Version; 
  ackProps["ad"] = "desired property received";
  reportedProperties["targetTemperature"] = ackProps;

  await client.UpdateReportedPropertiesAsync(reportedProperties);
}, null);

Bliźniacze reprezentacje urządzenia dla zagnieżdżonego składnika przedstawiają żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "targetTemperature": 23.2,
    "$version" : 3
  },
  "reported": {
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
  }
}

Zagnieżdżony składnik odbiera żądane właściwości opakowane nazwą składnika i powinien zgłosić raport z powrotem zgłoszoną ack właściwość:

await client.SetDesiredPropertyUpdateCallbackAsync(async (desired, ctx) =>
{
  JObject thermostatComponent = desired["thermostat1"];
  JToken targetTempProp = thermostatComponent["targetTemperature"];
  double targetTemperature = targetTempProp.Value<double>();

  TwinCollection reportedProperties = new TwinCollection();
  TwinCollection component = new TwinCollection();
  TwinCollection ackProps = new TwinCollection();
  component["__t"] = "c"; // marker to identify a component
  ackProps["value"] = targetTemperature;
  ackProps["ac"] = 200; // using HTTP status codes
  ackProps["av"] = desired.Version; // not readed from a desired property
  ackProps["ad"] = "desired property received";
  component["targetTemperature"] = ackProps;
  reportedProperties["thermostat1"] = component;

  await client.UpdateReportedPropertiesAsync(reportedProperties);
}, null);

Bliźniacze reprezentacje urządzenia dla zagnieżdżonego składnika przedstawiają żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "thermostat1" : {
        "__t" : "c",
        "targetTemperature": 23.2,
    }
    "$version" : 3
  },
  "reported": {
    "thermostat1" : {
        "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
    }
  }
}

Polecenia

Domyślny składnik otrzymuje nazwę polecenia, która została wywołana przez usługę.

Zagnieżdżony składnik otrzymuje nazwę polecenia poprzedzoną nazwą składnika i separatorem * .

await client.SetMethodHandlerAsync("themostat*reboot", (MethodRequest req, object ctx) =>
{
  Console.WriteLine("REBOOT");
  return Task.FromResult(new MethodResponse(200));
},
null);

Ładunki żądań i odpowiedzi

Polecenia używają typów do definiowania ładunków żądań i odpowiedzi. Urządzenie musi deserializować przychodzący parametr wejściowy i serializować odpowiedź.

W poniższym przykładzie pokazano, jak zaimplementować polecenie ze złożonymi typami zdefiniowanymi w ładunkach:

{
  "@type": "Command",
  "name": "start",
  "request": {
    "name": "startRequest",
    "schema": {
      "@type": "Object",
      "fields": [
        {
          "name": "startPriority",
          "schema": "integer"
        },
        {
          "name": "startMessage",
          "schema" : "string"
        }
      ]
    }
  },
  "response": {
    "name": "startReponse",
    "schema": {
      "@type": "Object",
      "fields": [
        {
            "name": "startupTime",
            "schema": "integer" 
        },
        {
          "name": "startupMessage",
          "schema": "string"
        }
      ]
    }
  }
}

Poniższe fragmenty kodu pokazują, jak urządzenie implementuje tę definicję polecenia, w tym typy używane do serializacji i deserializacji:

class startRequest
{
  public int startPriority { get; set; }
  public string startMessage { get; set; }
}

class startResponse
{
  public int startupTime { get; set; }
  public string startupMessage { get; set; }
}

// ... 

await client.SetMethodHandlerAsync("start", (MethodRequest req, object ctx) =>
{
  var startRequest = JsonConvert.DeserializeObject<startRequest>(req.DataAsJson);
  Console.WriteLine($"Received start command with priority ${startRequest.startPriority} and ${startRequest.startMessage}");

  var startResponse = new startResponse
  {
    startupTime = 123,
    startupMessage = "device started with message " + startRequest.startMessage
  };

  string responsePayload = JsonConvert.SerializeObject(startResponse);
  MethodResponse response = new MethodResponse(Encoding.UTF8.GetBytes(responsePayload), 200);
  return Task.FromResult(response);
},null);

Napiwek

Nazwy żądań i odpowiedzi nie są obecne w serializowanych ładunkach przesyłanych za pośrednictwem przewodu.

Anons identyfikatora modelu

Aby ogłosić identyfikator modelu, urządzenie musi je uwzględnić w informacjach o połączeniu:

ClientOptions options = new ClientOptions();
options.setModelId(MODEL_ID);
deviceClient = new DeviceClient(deviceConnectionString, protocol, options);

Przeciążenie ClientOptions jest dostępne we wszystkich DeviceClient metodach używanych do inicjowania połączenia.

Napiwek

W przypadku modułów i usługi IoT Edge należy użyć zamiast DeviceClientelementu ModuleClient .

Napiwek

Jest to jedyny czas, gdy urządzenie może ustawić identyfikator modelu, nie można go zaktualizować po nawiązaniu połączenia z urządzeniem.

Ładunek DPS

Urządzenia korzystające z usługi Device Provisioning Service (DPS) mogą zawierać modelId wartość, która ma być używana podczas procesu aprowizacji przy użyciu następującego ładunku JSON.

{
    "modelId" : "dtmi:com:example:Thermostat;1"
}

Używanie składników

Zgodnie z opisem w artykule Opis składników w modelach usługi IoT Plug and Play należy zdecydować, czy chcesz używać składników do opisywania urządzeń. W przypadku używania składników urządzenia muszą przestrzegać reguł opisanych w poniższych sekcjach.

Telemetria

Składnik domyślny nie wymaga żadnej specjalnej właściwości dodanej do komunikatu telemetrii.

W przypadku używania składników zagnieżdżonych urządzenie musi ustawić właściwość komunikatu o nazwie składnika:

private static void sendTemperatureTelemetry(String componentName) {
  double currentTemperature = temperature.get(componentName);

  Map<String, Object> payload = singletonMap("temperature", currentTemperature);

  Message message = new Message(gson.toJson(payload));
  message.setContentEncoding("utf-8");
  message.setContentTypeFinal("application/json");

  if (componentName != null) {
      message.setProperty("$.sub", componentName);
  }
  deviceClient.sendEventAsync(message, new MessageIotHubEventCallback(), message);
}

Właściwości tylko do odczytu

Raportowanie właściwości z domyślnego składnika nie wymaga żadnej specjalnej konstrukcji:

Property reportedProperty = new Property("maxTempSinceLastReboot", 38.7);

deviceClient.sendReportedProperties(Collections.singleton(reportedProperty));

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "maxTempSinceLastReboot" : 38.7
  }
}

W przypadku używania składników zagnieżdżonych utwórz właściwości w nazwie składnika i dołącz znacznik:

Map<String, Object> componentProperty = new HashMap<String, Object>() {{
    put("__t", "c");
    put("maxTemperature", 38.7);
}};

Set<Property> reportedProperty = new Property("thermostat1", componentProperty)

deviceClient.sendReportedProperties(reportedProperty);

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1" : {  
      "__t" : "c",  
      "maxTemperature" : 38.7
     }
  }
}

Właściwości z możliwością zapisu

Te właściwości można ustawić na urządzeniu lub zaktualizować przez aplikację zaplecza. Jeśli aplikacja zaplecza aktualizuje właściwość, klient otrzymuje powiadomienie jako wywołanie zwrotne w elemecie DeviceClient lub ModuleClient. Aby postępować zgodnie z konwencjami IoT Plug and Play, urządzenie musi poinformować usługę, że właściwość została pomyślnie odebrana.

Jeśli typ właściwości to Object, usługa musi wysłać pełny obiekt do urządzenia, nawet jeśli aktualizuje tylko podzestaw pól obiektu. Potwierdzenie wysyłane przez urządzenie musi być również kompletnym obiektem.

Zgłaszanie właściwości możliwej do zapisu

Gdy urządzenie zgłasza właściwość zapisywalną, musi zawierać ack wartości zdefiniowane w konwencjach.

Aby zgłosić właściwość zapisywalną z domyślnego składnika:

@AllArgsConstructor
private static class EmbeddedPropertyUpdate {
  @NonNull
  @SerializedName("value")
  public Object value;
  @NonNull
  @SerializedName("ac")
  public Integer ackCode;
  @NonNull
  @SerializedName("av")
  public Integer ackVersion;
  @SerializedName("ad")
  public String ackDescription;
}

EmbeddedPropertyUpdate completedUpdate = new EmbeddedPropertyUpdate(23.2, 200, 3, "Successfully updated target temperature");
Property reportedPropertyCompleted = new Property("targetTemperature", completedUpdate);
deviceClient.sendReportedProperties(Collections.singleton(reportedPropertyCompleted));

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "Successfully updated target temperature"
      }
  }
}

Aby zgłosić właściwość zapisywalną ze składnika zagnieżdżonego, reprezentacja bliźniacze musi zawierać znacznik:

Map<String, Object> embeddedProperty = new HashMap<String, Object>() {{
    put("value", 23.2);
    put("ac", 200);
    put("av", 3);
    put("ad", "complete");
}};

Map<String, Object> componentProperty = new HashMap<String, Object>() {{
    put("__t", "c");
    put("targetTemperature", embeddedProperty);
}};

Set<Property> reportedProperty = new Property("thermostat1", componentProperty));

deviceClient.sendReportedProperties(reportedProperty);

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1": {
      "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
    }
  }
}

Subskrybowanie żądanych aktualizacji właściwości

Usługi mogą aktualizować żądane właściwości, które wyzwalają powiadomienie na połączonych urządzeniach. To powiadomienie zawiera zaktualizowane żądane właściwości, w tym numer wersji identyfikujący aktualizację. Urządzenia muszą zawierać ten numer wersji w ack komunikacie wysłanym z powrotem do usługi.

Składnik domyślny widzi pojedynczą właściwość i tworzy raport ack z odebraną wersją:

private static class TargetTemperatureUpdateCallback implements TwinPropertyCallBack {

    String propertyName = "targetTemperature";

    @Override
    public void TwinPropertyCallBack(Property property, Object context) {
        double targetTemperature = ((Number)property.getValue()).doubleValue();

        EmbeddedPropertyUpdate completedUpdate = new EmbeddedPropertyUpdate(temperature, 200, property.getVersion(), "Successfully updated target temperature");
        Property reportedPropertyCompleted = new Property(propertyName, completedUpdate);
        deviceClient.sendReportedProperties(Collections.singleton(reportedPropertyCompleted));
    }
}

// ...

deviceClient.startDeviceTwin(new TwinIotHubEventCallback(), null, new TargetTemperatureUpdateCallback(), null);
Map<Property, Pair<TwinPropertyCallBack, Object>> desiredPropertyUpdateCallback =
  Collections.singletonMap(
    new Property("targetTemperature", null),
    new Pair<>(new TargetTemperatureUpdateCallback(), null));
deviceClient.subscribeToTwinDesiredProperties(desiredPropertyUpdateCallback);

Bliźniacze reprezentacje urządzenia dla zagnieżdżonego składnika przedstawiają żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "targetTemperature": 23.2,
    "$version" : 3
  },
  "reported": {
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "Successfully updated target temperature"
      }
  }
}

Zagnieżdżony składnik odbiera żądane właściwości opakowane nazwą składnika i powinien zgłosić raport z powrotem zgłoszoną ack właściwość:

private static final Map<String, Double> temperature = new HashMap<>();

private static class TargetTemperatureUpdateCallback implements TwinPropertyCallBack {

    String propertyName = "targetTemperature";

    @Override
    public void TwinPropertyCallBack(Property property, Object context) {
        String componentName = (String) context;

        if (property.getKey().equalsIgnoreCase(componentName)) {
            double targetTemperature = (double) ((TwinCollection) property.getValue()).get(propertyName);

            Map<String, Object> embeddedProperty = new HashMap<String, Object>() {{
                put("value", temperature.get(componentName));
                put("ac", 200);
                put("av", property.getVersion().longValue());
                put("ad", "Successfully updated target temperature.");
            }};

            Map<String, Object> componentProperty = new HashMap<String, Object>() {{
                put("__t", "c");
                put(propertyName, embeddedProperty);
            }};

            Set<Property> completedPropertyPatch = new Property(componentName, componentProperty));

            deviceClient.sendReportedProperties(completedPropertyPatch);
        } else {
            log.debug("Property: Received an unrecognized property update from service.");
        }
    }
}

// ...

deviceClient.startDeviceTwin(new TwinIotHubEventCallback(), null, new GenericPropertyUpdateCallback(), null);
Map<Property, Pair<TwinPropertyCallBack, Object>> desiredPropertyUpdateCallback = Stream.of(
  new AbstractMap.SimpleEntry<Property, Pair<TwinPropertyCallBack, Object>>(
    new Property("thermostat1", null),
    new Pair<>(new TargetTemperatureUpdateCallback(), "thermostat1")),
  new AbstractMap.SimpleEntry<Property, Pair<TwinPropertyCallBack, Object>>(
    new Property("thermostat2", null),
    new Pair<>(new TargetTemperatureUpdateCallback(), "thermostat2"))
).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));

deviceClient.subscribeToTwinDesiredProperties(desiredPropertyUpdateCallback);

Bliźniacze reprezentacje urządzenia dla zagnieżdżonego składnika przedstawiają żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "thermostat1" : {
        "__t" : "c",
        "targetTemperature": 23.2,
    }
    "$version" : 3
  },
  "reported": {
    "thermostat1" : {
        "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
    }
  }
}

Polecenia

Domyślny składnik otrzymuje nazwę polecenia, która została wywołana przez usługę.

Zagnieżdżony składnik otrzymuje nazwę polecenia poprzedzoną nazwą składnika i separatorem * .

deviceClient.subscribeToDeviceMethod(new MethodCallback(), null, new MethodIotHubEventCallback(), null);

// ...
private static final Map<String, Double> temperature = new HashMap<>();

private static class MethodCallback implements DeviceMethodCallback {
  final String reboot = "reboot";
  final String getMaxMinReport1 = "thermostat1*getMaxMinReport";
  final String getMaxMinReport2 = "thermostat2*getMaxMinReport";

  @Override
  public DeviceMethodData call(String methodName, Object methodData, Object context) {
    String jsonRequest = new String((byte[]) methodData, StandardCharsets.UTF_8);

    switch (methodName) {
      case reboot:
        int delay = gson.fromJson(jsonRequest, Integer.class);

        Thread.sleep(delay * 1000);

        temperature.put("thermostat1", 0.0d);
        temperature.put("thermostat2", 0.0d);

        return new DeviceMethodData(200, null);

      // ...

      default:
        log.debug("Command: command=\"{}\" is not implemented, no action taken.", methodName);
          return new DeviceMethodData(404, null);
    }
  }
}

Ładunki żądań i odpowiedzi

Polecenia używają typów do definiowania ładunków żądań i odpowiedzi. Urządzenie musi deserializować przychodzący parametr wejściowy i serializować odpowiedź.

W poniższym przykładzie pokazano, jak zaimplementować polecenie ze złożonymi typami zdefiniowanymi w ładunkach:

{
  "@type": "Command",
  "name": "getMaxMinReport",
  "displayName": "Get Max-Min report.",
  "description": "This command returns the max, min and average temperature from the specified time to the current time.",
  "request": {
    "name": "since",
    "displayName": "Since",
    "description": "Period to return the max-min report.",
    "schema": "dateTime"
  },
  "response": {
    "name" : "tempReport",
    "displayName": "Temperature Report",
    "schema": {
      "@type": "Object",
      "fields": [
        {
          "name": "maxTemp",
          "displayName": "Max temperature",
          "schema": "double"
        },
        {
          "name": "minTemp",
          "displayName": "Min temperature",
          "schema": "double"
        },
        {
          "name" : "avgTemp",
          "displayName": "Average Temperature",
          "schema": "double"
        },
        {
          "name" : "startTime",
          "displayName": "Start Time",
          "schema": "dateTime"
        },
        {
          "name" : "endTime",
          "displayName": "End Time",
          "schema": "dateTime"
        }
      ]
    }
  }
}

Poniższe fragmenty kodu pokazują, jak urządzenie implementuje tę definicję polecenia, w tym typy używane do serializacji i deserializacji:

deviceClient.subscribeToDeviceMethod(new GetMaxMinReportMethodCallback(), "getMaxMinReport", new MethodIotHubEventCallback(), "getMaxMinReport");

// ...

private static class GetMaxMinReportMethodCallback implements DeviceMethodCallback {
    String commandName = "getMaxMinReport";

    @Override
    public DeviceMethodData call(String methodName, Object methodData, Object context) {

        String jsonRequest = new String((byte[]) methodData, StandardCharsets.UTF_8);
        Date since = gson.fromJson(jsonRequest, Date.class);

        String responsePayload = String.format(
                "{\"maxTemp\": %.1f, \"minTemp\": %.1f, \"avgTemp\": %.1f, \"startTime\": \"%s\", \"endTime\": \"%s\"}",
                maxTemp,
                minTemp,
                avgTemp,
                since,
                endTime);

        return new DeviceMethodData(StatusCode.COMPLETED.value, responsePayload);
    }
}

Napiwek

Nazwy żądań i odpowiedzi nie są obecne w serializowanych ładunkach przesyłanych za pośrednictwem przewodu.

Anons identyfikatora modelu

Aby ogłosić identyfikator modelu, urządzenie musi je uwzględnić w informacjach o połączeniu:

const modelIdObject = { modelId: 'dtmi:com:example:Thermostat;1' };
const client = Client.fromConnectionString(deviceConnectionString, Protocol);
await client.setOptions(modelIdObject);
await client.open();

Napiwek

W przypadku modułów i usługi IoT Edge należy użyć zamiast Clientelementu ModuleClient .

Napiwek

Jest to jedyny czas, gdy urządzenie może ustawić identyfikator modelu, nie można go zaktualizować po nawiązaniu połączenia z urządzeniem.

Ładunek DPS

Urządzenia korzystające z usługi Device Provisioning Service (DPS) mogą zawierać modelId wartość, która ma być używana podczas procesu aprowizacji przy użyciu następującego ładunku JSON.

{
    "modelId" : "dtmi:com:example:Thermostat;1"
}

Używanie składników

Zgodnie z opisem w artykule Opis składników w modelach usługi IoT Plug and Play należy zdecydować, czy chcesz używać składników do opisywania urządzeń. W przypadku używania składników urządzenia muszą przestrzegać reguł opisanych w poniższych sekcjach.

Telemetria

Składnik domyślny nie wymaga żadnej specjalnej właściwości dodanej do komunikatu telemetrii.

W przypadku używania składników zagnieżdżonych urządzenia muszą ustawić właściwość komunikatu o nazwie składnika:

async function sendTelemetry(deviceClient, data, index, componentName) {
  const msg = new Message(data);
  if (!!(componentName)) {
    msg.properties.add(messageSubjectProperty, componentName);
  }
  msg.contentType = 'application/json';
  msg.contentEncoding = 'utf-8';
  await deviceClient.sendEvent(msg);
}

Właściwości tylko do odczytu

Raportowanie właściwości z domyślnego składnika nie wymaga żadnej specjalnej konstrukcji:

const createReportPropPatch = (propertiesToReport) => {
  let patch;
  patch = { };
  patch = propertiesToReport;
  return patch;
};

deviceTwin = await client.getTwin();
patchThermostat = createReportPropPatch({
  maxTempSinceLastReboot: 38.7
});

deviceTwin.properties.reported.update(patchThermostat, function (err) {
  if (err) throw err;
});

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "maxTempSinceLastReboot" : 38.7
  }
}

W przypadku używania składników zagnieżdżonych właściwości należy utworzyć w nazwie składnika i dołączyć znacznik:

helperCreateReportedPropertiesPatch = (propertiesToReport, componentName) => {
  let patch;
  if (!!(componentName)) {
    patch = { };
    propertiesToReport.__t = 'c';
    patch[componentName] = propertiesToReport;
  } else {
    patch = { };
    patch = propertiesToReport;
  }
  return patch;
};

deviceTwin = await client.getTwin();
patchThermostat1Info = helperCreateReportedPropertiesPatch({
  maxTempSinceLastReboot: 38.7,
}, 'thermostat1');

deviceTwin.properties.reported.update(patchThermostat1Info, function (err) {
  if (err) throw err;
});

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1" : {  
      "__t" : "c",  
      "maxTempSinceLastReboot" : 38.7
     } 
  }
}

Właściwości z możliwością zapisu

Te właściwości można ustawić na urządzeniu lub zaktualizować przez aplikację zaplecza. Jeśli aplikacja zaplecza aktualizuje właściwość, klient otrzymuje powiadomienie jako wywołanie zwrotne w elemecie Client lub ModuleClient. Aby postępować zgodnie z konwencjami IoT Plug and Play, urządzenie musi poinformować usługę, że właściwość została pomyślnie odebrana.

Jeśli typ właściwości to Object, usługa musi wysłać pełny obiekt do urządzenia, nawet jeśli aktualizuje tylko podzestaw pól obiektu. Potwierdzenie wysyłane przez urządzenie musi być również kompletnym obiektem.

Zgłaszanie właściwości możliwej do zapisu

Gdy urządzenie zgłasza właściwość zapisywalną, musi zawierać ack wartości zdefiniowane w konwencjach.

Aby zgłosić właściwość zapisywalną z domyślnego składnika:

patch = {
  targetTemperature:
    {
      'value': 23.2,
      'ac': 200,  // using HTTP status codes
      'ad': 'reported default value',
      'av': 0  // not read from a desired property
    }
};
deviceTwin.properties.reported.update(patch, function (err) {
  if (err) throw err;
});

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "targetTemperature": {
      "value": 23.2,
      "ac": 200,
      "av": 0,
      "ad": "reported default value"
    }
  }
}

Aby zgłosić właściwość zapisywalną ze składnika zagnieżdżonego, reprezentacja bliźniacze musi zawierać znacznik:

patch = {
  thermostat1: {
    '__t' : 'c',
    targetTemperature: {
      'value': 23.2,
      'ac': 200,  // using HTTP status codes
      'ad': 'reported default value',
      'av': 0  // not read from a desired property
    }
  }
};
deviceTwin.properties.reported.update(patch, function (err) {
  if (err) throw err;
});

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1": {
      "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 0,
          "ad": "complete"
      }
    }
  }
}

Subskrybowanie żądanych aktualizacji właściwości

Usługi mogą aktualizować żądane właściwości, które wyzwalają powiadomienie na połączonych urządzeniach. To powiadomienie zawiera zaktualizowane żądane właściwości, w tym numer wersji identyfikujący aktualizację. Urządzenia muszą zawierać ten numer wersji w ack komunikacie wysłanym z powrotem do usługi.

Składnik domyślny widzi pojedynczą właściwość i tworzy raport ack z odebraną wersją:

const propertyUpdateHandler = (deviceTwin, propertyName, reportedValue, desiredValue, version) => {
  const patch = createReportPropPatch(
    { [propertyName]:
      {
        'value': desiredValue,
        'ac': 200,
        'ad': 'Successfully executed patch for ' + propertyName,
        'av': version
      }
    });
  updateComponentReportedProperties(deviceTwin, patch);
};

desiredPropertyPatchHandler = (deviceTwin) => {
  deviceTwin.on('properties.desired', (delta) => {
    const versionProperty = delta.$version;

    Object.entries(delta).forEach(([propertyName, propertyValue]) => {
      if (propertyName !== '$version') {
        propertyUpdateHandler(deviceTwin, propertyName, null, propertyValue, versionProperty);
      }
    });
  });
};

Bliźniacze reprezentacje urządzenia dla zagnieżdżonego składnika przedstawiają żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "targetTemperature": 23.2,
    "$version" : 3
  },
  "reported": {
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
  }
}

Zagnieżdżony składnik odbiera żądane właściwości opakowane nazwą składnika i powinien zgłosić raport z powrotem zgłoszoną ack właściwość:

const desiredPropertyPatchListener = (deviceTwin, componentNames) => {
  deviceTwin.on('properties.desired', (delta) => {
    Object.entries(delta).forEach(([key, values]) => {
      const version = delta.$version;
      if (!!(componentNames) && componentNames.includes(key)) { // then it is a component we are expecting
        const componentName = key;
        const patchForComponents = { [componentName]: {} };
        Object.entries(values).forEach(([propertyName, propertyValue]) => {
          if (propertyName !== '__t' && propertyName !== '$version') {
            const propertyContent = { value: propertyValue };
            propertyContent.ac = 200;
            propertyContent.ad = 'Successfully executed patch';
            propertyContent.av = version;
            patchForComponents[componentName][propertyName] = propertyContent;
          }
        });
        updateComponentReportedProperties(deviceTwin, patchForComponents, componentName);
      }
      else if  (key !== '$version') { // individual property for root
        const patchForRoot = { };
        const propertyContent = { value: values };
        propertyContent.ac = 200;
        propertyContent.ad = 'Successfully executed patch';
        propertyContent.av = version;
        patchForRoot[key] = propertyContent;
        updateComponentReportedProperties(deviceTwin, patchForRoot, null);
      }
    });
  });
};

Bliźniacze reprezentacje urządzenia dla składników pokazują żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "thermostat1" : {
        "__t" : "c",
        "targetTemperature": 23.2,
    }
    "$version" : 3
  },
  "reported": {
    "thermostat1" : {
        "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
    }
  }
}

Polecenia

Domyślny składnik otrzymuje nazwę polecenia, która została wywołana przez usługę.

Zagnieżdżony składnik otrzymuje nazwę polecenia poprzedzoną nazwą składnika i separatorem * .

const commandHandler = async (request, response) => {
  switch (request.methodName) {
  
  // ...

  case 'thermostat1*reboot': {
    await response.send(200, 'reboot response');
    break;
  }
  default:
    await response.send(404, 'unknown method');
    break;
  }
};

client.onDeviceMethod('thermostat1*reboot', commandHandler);

Ładunki żądań i odpowiedzi

Polecenia używają typów do definiowania ładunków żądań i odpowiedzi. Urządzenie musi deserializować przychodzący parametr wejściowy i serializować odpowiedź.

W poniższym przykładzie pokazano, jak zaimplementować polecenie ze złożonymi typami zdefiniowanymi w ładunkach:

{
  "@type": "Command",
  "name": "getMaxMinReport",
  "displayName": "Get Max-Min report.",
  "description": "This command returns the max, min and average temperature from the specified time to the current time.",
  "request": {
    "name": "since",
    "displayName": "Since",
    "description": "Period to return the max-min report.",
    "schema": "dateTime"
  },
  "response": {
    "name" : "tempReport",
    "displayName": "Temperature Report",
    "schema": {
      "@type": "Object",
      "fields": [
        {
          "name": "maxTemp",
          "displayName": "Max temperature",
          "schema": "double"
        },
        {
          "name": "minTemp",
          "displayName": "Min temperature",
          "schema": "double"
        },
        {
          "name" : "avgTemp",
          "displayName": "Average Temperature",
          "schema": "double"
        },
        {
          "name" : "startTime",
          "displayName": "Start Time",
          "schema": "dateTime"
        },
        {
          "name" : "endTime",
          "displayName": "End Time",
          "schema": "dateTime"
        }
      ]
    }
  }
}

Poniższe fragmenty kodu pokazują, jak urządzenie implementuje tę definicję polecenia, w tym typy używane do serializacji i deserializacji:

class TemperatureSensor {

  // ...

  getMaxMinReportObject() {
    return {
      maxTemp: this.maxTemp,
      minTemp: this.minTemp,
      avgTemp: this.cumulativeTemperature / this.numberOfTemperatureReadings,
      endTime: (new Date(Date.now())).toISOString(),
      startTime: this.startTime
    };
  }
}

// ...

const deviceTemperatureSensor = new TemperatureSensor();

const commandHandler = async (request, response) => {
  switch (request.methodName) {
  case commandMaxMinReport: {
    console.log('MaxMinReport ' + request.payload);
    await response.send(200, deviceTemperatureSensor.getMaxMinReportObject());
    break;
  }
  default:
    await response.send(404, 'unknown method');
    break;
  }
};

Napiwek

Nazwy żądań i odpowiedzi nie są obecne w serializowanych ładunkach przesyłanych za pośrednictwem przewodu.

Anons identyfikatora modelu

Aby ogłosić identyfikator modelu, urządzenie musi je uwzględnić w informacjach o połączeniu:

device_client = IoTHubDeviceClient.create_from_symmetric_key(
    symmetric_key=symmetric_key,
    hostname=registration_result.registration_state.assigned_hub,
    device_id=registration_result.registration_state.device_id,
    product_info=model_id,
)

Napiwek

W przypadku modułów i usługi IoT Edge należy użyć zamiast IoTHubDeviceClientelementu IoTHubModuleClient .

Napiwek

Jest to jedyny czas, gdy urządzenie może ustawić identyfikator modelu, nie można go zaktualizować po nawiązaniu połączenia z urządzeniem.

Ładunek DPS

Urządzenia korzystające z usługi Device Provisioning Service (DPS) mogą zawierać modelId wartość, która ma być używana podczas procesu aprowizacji przy użyciu następującego ładunku JSON.

{
    "modelId" : "dtmi:com:example:Thermostat;1"
}

Używanie składników

Zgodnie z opisem w artykule Opis składników w modelach usługi IoT Plug and Play należy zdecydować, czy chcesz używać składników do opisywania urządzeń. W przypadku używania składników urządzenia muszą przestrzegać reguł opisanych w poniższych sekcjach.

Telemetria

Składnik domyślny nie wymaga żadnej specjalnej właściwości dodanej do komunikatu telemetrii.

W przypadku używania składników zagnieżdżonych urządzenia muszą ustawić właściwość komunikatu o nazwie składnika:

async def send_telemetry_from_temp_controller(device_client, telemetry_msg, component_name=None):
    msg = Message(json.dumps(telemetry_msg))
    msg.content_encoding = "utf-8"
    msg.content_type = "application/json"
    if component_name:
        msg.custom_properties["$.sub"] = component_name
    await device_client.send_message(msg)

Właściwości tylko do odczytu

Raportowanie właściwości z domyślnego składnika nie wymaga żadnej specjalnej konstrukcji:

await device_client.patch_twin_reported_properties({"maxTempSinceLastReboot": 38.7})

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
      "maxTempSinceLastReboot" : 38.7
  }
}

W przypadku korzystania ze składników zagnieżdżonych właściwości należy utworzyć w nazwie składnika i dołączyć znacznik:

inner_dict = {}
inner_dict["targetTemperature"] = 38.7
inner_dict["__t"] = "c"
prop_dict = {}
prop_dict["thermostat1"] = inner_dict

await device_client.patch_twin_reported_properties(prop_dict)

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1" : {  
      "__t" : "c",  
      "maxTempSinceLastReboot" : 38.7
     }
  }
}

Właściwości z możliwością zapisu

Te właściwości można ustawić na urządzeniu lub zaktualizować przez aplikację zaplecza. Jeśli aplikacja zaplecza aktualizuje właściwość, klient otrzymuje powiadomienie jako wywołanie zwrotne w elemecie IoTHubDeviceClient lub IoTHubModuleClient. Aby postępować zgodnie z konwencjami IoT Plug and Play, urządzenie musi poinformować usługę, że właściwość została pomyślnie odebrana.

Jeśli typ właściwości to Object, usługa musi wysłać pełny obiekt do urządzenia, nawet jeśli aktualizuje tylko podzestaw pól obiektu. Potwierdzenie wysyłane przez urządzenie musi być również kompletnym obiektem.

Zgłaszanie właściwości możliwej do zapisu

Gdy urządzenie zgłasza właściwość zapisywalną, musi zawierać ack wartości zdefiniowane w konwencjach.

Aby zgłosić właściwość zapisywalną z domyślnego składnika:

prop_dict = {}
prop_dict["targetTemperature"] = {
    "ac": 200,
    "ad": "reported default value",
    "av": 0,
    "value": 23.2
}

await device_client.patch_twin_reported_properties(prop_dict)

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "targetTemperature": {
      "value": 23.2,
      "ac": 200,
      "av": 0,
      "ad": "reported default value"
    }
  }
}

Aby zgłosić właściwość zapisywalną ze składnika zagnieżdżonego, reprezentacja bliźniacze musi zawierać znacznik:

inner_dict = {}
inner_dict["targetTemperature"] = {
    "ac": 200,
    "ad": "reported default value",
    "av": 0,
    "value": 23.2
}
inner_dict["__t"] = "c"
prop_dict = {}
prop_dict["thermostat1"] = inner_dict

await device_client.patch_twin_reported_properties(prop_dict)

Bliźniacze reprezentacje urządzenia są aktualizowane przy użyciu następującej zgłaszanej właściwości:

{
  "reported": {
    "thermostat1": {
      "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 0,
          "ad": "complete"
      }
    }
  }
}

Subskrybowanie żądanych aktualizacji właściwości

Usługi mogą aktualizować żądane właściwości, które wyzwalają powiadomienie na połączonych urządzeniach. To powiadomienie zawiera zaktualizowane żądane właściwości, w tym numer wersji identyfikujący aktualizację. Urządzenia muszą zawierać ten numer wersji w ack komunikacie wysłanym z powrotem do usługi.

Składnik domyślny widzi pojedynczą właściwość i tworzy raport ack z odebraną wersją:

async def execute_property_listener(device_client):
    ignore_keys = ["__t", "$version"]
    while True:
        patch = await device_client.receive_twin_desired_properties_patch()  # blocking call

        version = patch["$version"]
        prop_dict = {}

        for prop_name, prop_value in patch.items():
            if prop_name in ignore_keys:
                continue
            else:
                prop_dict[prop_name] = {
                    "ac": 200,
                    "ad": "Successfully executed patch",
                    "av": version,
                    "value": prop_value,
                }

        await device_client.patch_twin_reported_properties(prop_dict)

Bliźniacze reprezentacje urządzenia dla zagnieżdżonego składnika przedstawiają żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "targetTemperature": 23.2,
    "$version" : 3
  },
  "reported": {
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
  }
}

Zagnieżdżony składnik odbiera żądane właściwości opakowane nazwą składnika i powinien zgłosić raport z powrotem zgłoszoną ack właściwość:

def create_reported_properties_from_desired(patch):
    ignore_keys = ["__t", "$version"]
    component_prefix = list(patch.keys())[0]
    values = patch[component_prefix]

    version = patch["$version"]
    inner_dict = {}

    for prop_name, prop_value in values.items():
        if prop_name in ignore_keys:
            continue
        else:
            inner_dict["ac"] = 200
            inner_dict["ad"] = "Successfully executed patch"
            inner_dict["av"] = version
            inner_dict["value"] = prop_value
            values[prop_name] = inner_dict

    properties_dict = dict()
    if component_prefix:
        properties_dict[component_prefix] = values
    else:
        properties_dict = values

    return properties_dict

async def execute_property_listener(device_client):
    while True:
        patch = await device_client.receive_twin_desired_properties_patch()  # blocking call
        properties_dict = create_reported_properties_from_desired(patch)

        await device_client.patch_twin_reported_properties(properties_dict)

Bliźniacze reprezentacje urządzenia dla składników pokazują żądane i zgłoszone sekcje w następujący sposób:

{
  "desired" : {
    "thermostat1" : {
        "__t" : "c",
        "targetTemperature": 23.2,
    }
    "$version" : 3
  },
  "reported": {
    "thermostat1" : {
        "__t" : "c",
      "targetTemperature": {
          "value": 23.2,
          "ac": 200,
          "av": 3,
          "ad": "complete"
      }
    }
  }
}

Polecenia

Domyślny składnik otrzymuje nazwę polecenia, która została wywołana przez usługę.

Zagnieżdżony składnik otrzymuje nazwę polecenia poprzedzoną nazwą składnika i separatorem * .

command_request = await device_client.receive_method_request("thermostat1*reboot")

Ładunki żądań i odpowiedzi

Polecenia używają typów do definiowania ładunków żądań i odpowiedzi. Urządzenie musi deserializować przychodzący parametr wejściowy i serializować odpowiedź.

W poniższym przykładzie pokazano, jak zaimplementować polecenie ze złożonymi typami zdefiniowanymi w ładunkach:

{
  "@type": "Command",
  "name": "getMaxMinReport",
  "displayName": "Get Max-Min report.",
  "description": "This command returns the max, min and average temperature from the specified time to the current time.",
  "request": {
    "name": "since",
    "displayName": "Since",
    "description": "Period to return the max-min report.",
    "schema": "dateTime"
  },
  "response": {
    "name" : "tempReport",
    "displayName": "Temperature Report",
    "schema": {
      "@type": "Object",
      "fields": [
        {
          "name": "maxTemp",
          "displayName": "Max temperature",
          "schema": "double"
        },
        {
          "name": "minTemp",
          "displayName": "Min temperature",
          "schema": "double"
        },
        {
          "name" : "avgTemp",
          "displayName": "Average Temperature",
          "schema": "double"
        },
        {
          "name" : "startTime",
          "displayName": "Start Time",
          "schema": "dateTime"
        },
        {
          "name" : "endTime",
          "displayName": "End Time",
          "schema": "dateTime"
        }
      ]
    }
  }
}

Poniższe fragmenty kodu pokazują, jak urządzenie implementuje tę definicję polecenia, w tym typy używane do serializacji i deserializacji:

def create_max_min_report_response(values):
    response_dict = {
        "maxTemp": max_temp,
        "minTemp": min_temp,
        "avgTemp": sum(avg_temp_list) / moving_window_size,
        "startTime": (datetime.now() - timedelta(0, moving_window_size * 8)).isoformat(),
        "endTime": datetime.now().isoformat(),
    }
    # serialize response dictionary into a JSON formatted str
    response_payload = json.dumps(response_dict, default=lambda o: o.__dict__, sort_keys=True)
    return response_payload

Napiwek

Nazwy żądań i odpowiedzi nie są obecne w serializowanych ładunkach przesyłanych za pośrednictwem przewodu.

Następne kroki

Teraz, gdy już wiesz już, jak opracowywać urządzenia IoT Plug and Play, oto kilka innych zasobów: