您现在访问的是微软AZURE全球版技术文档网站,若需要访问由世纪互联运营的MICROSOFT AZURE中国区技术文档网站,请访问 https://docs.azure.cn.

IoT 即插即用设备开发人员指南IoT Plug and Play device developer guide

IoT 即插即用允许构建智能设备,将其功能公布到 Azure IoT 应用程序。IoT Plug and Play lets you build smart devices that advertise their capabilities to Azure IoT applications. 当客户将其连接到 IoT 即插即用启用的应用程序时,IoT 即插即用设备不需要手动配置。IoT Plug and Play devices don't require manual configuration when a customer connects them to IoT Plug and Play-enabled applications.

智能设备可以直接实现、使用 模块或使用 IoT Edge 模块A smart device might be implemented directly, use modules, or use IoT Edge modules.

本指南介绍了按照 IoT 即插即用约定创建设备、模块或 IoT Edge 模块所需的基本步骤。This guide describes the basic steps required to create a device, module, or IoT Edge module that follows the IoT Plug and Play conventions.

若要构建 IoT 即插即用设备、模块或 IoT Edge 模块,请执行以下步骤:To build an IoT Plug and Play device, module, or IoT Edge module, follow these steps:

  1. 确保你的设备使用 MQTT 或 MQTT over Websocket 协议连接到 Azure IoT 中心。Ensure your device is using either the MQTT or MQTT over WebSockets protocol to connect to Azure IoT Hub.
  2. 创建 数字孪生定义语言 (DTDL) 模型来描述你的设备。Create a Digital Twins Definition Language (DTDL) model to describe your device. 若要了解详细信息,请参阅 了解 IoT 即插即用模型中的组件To learn more, see Understand components in IoT Plug and Play models.
  3. 更新设备或模块,以便在 model-id 设备连接过程中通告。Update your device or module to announce the model-id as part of the device connection.
  4. 使用IoT 即插即用约定实现遥测、属性和命令Implement telemetry, properties, and commands using the IoT Plug and Play conventions

设备或模块实现准备就绪后,请使用 Azure iot 浏览器 验证设备是否遵循 IoT 即插即用约定。Once your device or module implementation is ready, use the Azure IoT explorer to validate that the device follows the IoT Plug and Play conventions.

模型 ID 公告Model ID announcement

若要公布模型 ID,设备必须将其包含在连接信息中:To announce the model ID, the device must include it in the connection information:

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);

提示

对于模块和 IoT Edge,使用 IoTHubModuleClient_LL 代替 IoTHubDeviceClient_LLFor modules and IoT Edge, use IoTHubModuleClient_LL in place of IoTHubDeviceClient_LL.

提示

这是设备可以设置模型 ID 的唯一时间,在设备连接后将无法更新。This is the only time a device can set model ID, it can't be updated after the device connects.

DPS 有效负载DPS payload

使用 设备预配服务 (DPS) 的设备可以包括 modelId 使用以下 JSON 有效负载的预配过程中要使用的。Devices using the Device Provisioning Service (DPS) can include the modelId to be used during the provisioning process using the following JSON payload.

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

实现遥测、属性和命令Implement telemetry, properties, and commands

了解 IoT 即插即用模型中的组件中所述,设备构建者必须决定是否要使用组件来描述其设备。As described in Understand components in IoT Plug and Play models, device builders must decide if they want to use components to describe their devices. 使用组件时,设备必须遵循本部分中所述的规则。When using components, devices must follow the rules described in this section.

遥测Telemetry

默认组件不需要任何特殊属性。A default component doesn't require any special property.

使用嵌套组件时,设备必须使用组件名称设置消息属性:When using nested components, devices must set a message property with the component name:

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);

只读属性Read-only properties

从默认组件报告属性不需要任何特殊构造:Reporting a property from the default component doesn't require any special construct:

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));

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

使用嵌套组件时,必须在组件名称中创建属性:When using nested components, properties must be created within the component name:

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);

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

可写属性Writable properties

这些属性可以由设备设置或通过解决方案更新。These properties can be set by the device or updated by the solution. 如果解决方案更新属性,则客户端会在或中接收到作为回调的 DeviceClient 通知 ModuleClientIf the solution updates a property, the client receives a notification as a callback in the DeviceClient or ModuleClient. 若要遵循 IoT 即插即用约定,设备必须通知服务属性已成功接收。To follow the IoT Plug and Play conventions, the device must inform the service that the property was successfully received.

报告可写属性Report a writable property

当设备报告可写属性时,它必须包含 ack 约定中定义的值。When a device reports a writable property, it must include the ack values defined in the conventions.

若要从默认组件报告可写属性:To report a writable property from the default component:

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);

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

若要从嵌套组件报告可写属性,则克隆必须包括标记:To report a writable property from a nested component, the twin must include a marker:

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);

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

订阅所需属性更新Subscribe to desired property updates

服务可以更新在连接的设备上触发通知的所需属性。Services can update desired properties that trigger a notification on the connected devices. 此通知包括更新的所需属性,其中包括用于标识更新的版本号。This notification includes the updated desired properties, including the version number identifying the update. 设备必须用与报告属性相同的消息进行响应 ackDevices must respond with the same ack message as reported properties.

默认组件将查看单个属性,并 ack 使用收到的版本创建报告:A default component sees the single property and creates the reported ack with the received version:

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))

设备克隆在所需的和报告的部分显示属性:The device twin shows the property in the desired and reported sections:

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

嵌套组件接收用组件名称包装的所需属性,并报告返回 ack 报告的属性:A nested component receives the desired properties wrapped with the component name, and should report back the ack reported property:

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);
}

嵌套组件的设备克隆显示了所需的和报告的部分,如下所示:The device twin for a nested component shows the desired and reported sections as follows:

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

命令Commands

默认组件接收服务调用的命令名称。A default component receives the command name as it was invoked by the service.

嵌套组件接收以组件名称和分隔符为前缀的命令名称 *A nested component receives the command name prefixed with the component name and the * separator.

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);

请求和响应负载Request and response payloads

命令使用类型来定义其请求和响应负载。Commands use types to define their request and response payloads. 设备必须反序列化传入的输入参数并序列化响应。A device must deserialize the incoming input parameter and serialize the response. 下面的示例演示如何实现具有负载中定义的复杂类型的命令:The following example shows how to implement a command with complex types defined in the payloads:

{
  "@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"
        }
      ]
    }
  }
}

下面的代码段演示了设备如何实现此命令定义,其中包括用于启用序列化和反序列化的类型:The following code snippets show how a device implements this command definition, including the types used to enable serialization and deserialization:

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;
}

提示

请求和响应名称不存在于通过网络传输的序列化有效负载中。The request and response names aren't present in the serialized payloads transmitted over the wire.

模型 ID 公告Model ID announcement

若要公布模型 ID,设备必须将其包含在连接信息中:To announce the model ID, the device must include it in the connection information:

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

ClientOptions 重载在 DeviceClient 用于初始化连接的所有方法中可用。The new ClientOptions overload is available in all DeviceClient methods used to initialize a connection.

提示

对于模块和 IoT Edge,使用 ModuleClient 代替 DeviceClientFor modules and IoT Edge, use ModuleClient in place of DeviceClient.

提示

这是设备可以设置模型 ID 的唯一时间,在设备连接后将无法更新。This is the only time a device can set model ID, it can't be updated after the device connects.

DPS 有效负载DPS payload

使用 设备预配服务 (DPS) 的设备可以包括 modelId 使用以下 JSON 有效负载的预配过程中要使用的。Devices using the Device Provisioning Service (DPS) can include the modelId to be used during the provisioning process using the following JSON payload.

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

实现遥测、属性和命令Implement telemetry, properties, and commands

了解 IoT 即插即用模型中的组件中所述,设备构建者必须决定是否要使用组件来描述其设备。As described in Understand components in IoT Plug and Play models, device builders must decide if they want to use components to describe their devices. 使用组件时,设备必须遵循本部分中所述的规则。When using components, devices must follow the rules described in this section.

遥测Telemetry

默认组件不需要任何特殊属性。A default component doesn't require any special property.

使用嵌套组件时,设备必须使用组件名称设置消息属性:When using nested components, devices must set a message property with the component name:

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);
}

只读属性Read-only properties

从默认组件报告属性不需要任何特殊构造:Reporting a property from the default component doesn't require any special construct:

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

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

使用嵌套组件时,必须在组件名称中创建属性:When using nested components, properties must be created within the component name:

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);

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

可写属性Writable properties

这些属性可以由设备设置或通过解决方案更新。These properties can be set by the device or updated by the solution. 如果解决方案更新属性,则客户端会在或中接收到作为回调的 DeviceClient 通知 ModuleClientIf the solution updates a property, the client receives a notification as a callback in the DeviceClient or ModuleClient. 若要遵循 IoT 即插即用约定,设备必须通知服务属性已成功接收。To follow the IoT Plug and Play conventions, the device must inform the service that the property was successfully received.

报告可写属性Report a writable property

当设备报告可写属性时,它必须包含 ack 约定中定义的值。When a device reports a writable property, it must include the ack values defined in the conventions.

若要从默认组件报告可写属性:To report a writable property from the default component:

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);

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

若要从嵌套组件报告可写属性,则克隆必须包括标记:To report a writable property from a nested component, the twin must include a marker:

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);

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

订阅所需属性更新Subscribe to desired property updates

服务可以更新在连接的设备上触发通知的所需属性。Services can update desired properties that trigger a notification on the connected devices. 此通知包括更新的所需属性,其中包括用于标识更新的版本号。This notification includes the updated desired properties, including the version number identifying the update. 设备必须用与报告属性相同的消息进行响应 ackDevices must respond with the same ack message as reported properties.

默认组件将查看单个属性,并 ack 使用收到的版本创建报告:A default component sees the single property and creates the reported ack with the received version:

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);

设备克隆在所需的和报告的部分显示属性:The device twin shows the property in the desired and reported sections:

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

嵌套组件接收用组件名称包装的所需属性,并报告返回 ack 报告的属性:A nested component receives the desired properties wrapped with the component name, and should report back the ack reported property:

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);

嵌套组件的设备克隆显示了所需的和报告的部分,如下所示:The device twin for a nested component shows the desired and reported sections as follows:

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

命令Commands

默认组件接收服务调用的命令名称。A default component receives the command name as it was invoked by the service.

嵌套组件接收以组件名称和分隔符为前缀的命令名称 *A nested component receives the command name prefixed with the component name and the * separator.

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

请求和响应负载Request and response payloads

命令使用类型来定义其请求和响应负载。Commands use types to define their request and response payloads. 设备必须反序列化传入的输入参数并序列化响应。A device must deserialize the incoming input parameter and serialize the response. 下面的示例演示如何实现具有负载中定义的复杂类型的命令:The following example shows how to implement a command with complex types defined in the payloads:

{
  "@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"
        }
      ]
    }
  }
}

下面的代码段演示了设备如何实现此命令定义,其中包括用于启用序列化和反序列化的类型:The following code snippets show how a device implements this command definition, including the types used to enable serialization and deserialization:

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);

提示

请求和响应名称不存在于通过网络传输的序列化有效负载中。The request and response names aren't present in the serialized payloads transmitted over the wire.

模型 ID 公告Model ID announcement

若要公布模型 ID,设备必须将其包含在连接信息中:To announce the model ID, the device must include it in the connection information:

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

ClientOptions重载在 DeviceClient 用于初始化连接的所有方法中可用。The ClientOptions overload is available in all DeviceClient methods used to initialize a connection.

提示

对于模块和 IoT Edge,使用 ModuleClient 代替 DeviceClientFor modules and IoT Edge, use ModuleClient in place of DeviceClient.

提示

这是设备可以设置模型 ID 的唯一时间,在设备连接后将无法更新。This is the only time a device can set model ID, it can't be updated after the device connects.

DPS 有效负载DPS payload

使用 设备预配服务 (DPS) 的设备可以包括 modelId 使用以下 JSON 有效负载的预配过程中要使用的。Devices using the Device Provisioning Service (DPS) can include the modelId to be used during the provisioning process using the following JSON payload.

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

实现遥测、属性和命令Implement telemetry, properties, and commands

了解 IoT 即插即用模型中的组件中所述,设备构建者必须决定是否要使用组件来描述其设备。As described in Understand components in IoT Plug and Play models, device builders must decide if they want to use components to describe their devices. 使用组件时,设备必须遵循本部分中所述的规则。When using components, devices must follow the rules described in this section.

遥测Telemetry

默认组件不需要任何特殊属性。A default component doesn't require any special property.

使用嵌套组件时,设备必须使用组件名称设置消息属性:When using nested components, devices must set a message property with the component name:

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);
}

只读属性Read-only properties

从默认组件报告属性不需要任何特殊构造:Reporting a property from the default component doesn't require any special construct:

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

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

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

使用嵌套组件时,必须在组件名称中创建属性:When using nested components, properties must be created within the component name:

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);

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

可写属性Writable properties

这些属性可以由设备设置或通过解决方案更新。These properties can be set by the device or updated by the solution. 如果解决方案更新属性,则客户端会在或中接收到作为回调的 DeviceClient 通知 ModuleClientIf the solution updates a property, the client receives a notification as a callback in the DeviceClient or ModuleClient. 若要遵循 IoT 即插即用约定,设备必须通知服务属性已成功接收。To follow the IoT Plug and Play conventions, the device must inform the service that the property was successfully received.

报告可写属性Report a writable property

当设备报告可写属性时,它必须包含 ack 约定中定义的值。When a device reports a writable property, it must include the ack values defined in the conventions.

若要从默认组件报告可写属性:To report a writable property from the default component:

@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));

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

若要从嵌套组件报告可写属性,则克隆必须包括标记:To report a writable property from a nested component, the twin must include a marker:

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);

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

订阅所需属性更新Subscribe to desired property updates

服务可以更新在连接的设备上触发通知的所需属性。Services can update desired properties that trigger a notification on the connected devices. 此通知包括更新的所需属性,其中包括用于标识更新的版本号。This notification includes the updated desired properties, including the version number identifying the update. 设备必须用与报告属性相同的消息进行响应 ackDevices must respond with the same ack message as reported properties.

默认组件将查看单个属性,并 ack 使用收到的版本创建报告:A default component sees the single property and creates the reported ack with the received version:

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);

设备克隆在所需的和报告的部分显示属性:The device twin shows the property in the desired and reported sections:

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

嵌套组件接收用组件名称包装的所需属性,并报告返回 ack 报告的属性:A nested component receives the desired properties wrapped with the component name, and should report back the ack reported property:

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);

嵌套组件的设备克隆显示了所需的和报告的部分,如下所示:The device twin for a nested component shows the desired and reported sections as follows:

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

命令Commands

默认组件接收服务调用的命令名称。A default component receives the command name as it was invoked by the service.

嵌套组件接收以组件名称和分隔符为前缀的命令名称 *A nested component receives the command name prefixed with the component name and the * separator.

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);
    }
  }
}

请求和响应负载Request and response payloads

命令使用类型来定义其请求和响应负载。Commands use types to define their request and response payloads. 设备必须反序列化传入的输入参数并序列化响应。A device must deserialize the incoming input parameter and serialize the response.

下面的示例演示如何实现具有负载中定义的复杂类型的命令:The following example shows how to implement a command with complex types defined in the payloads:

{
  "@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"
        }
      ]
    }
  }
}

下面的代码段演示了设备如何实现此命令定义,其中包括用于启用序列化和反序列化的类型:The following code snippets show how a device implements this command definition, including the types used to enable serialization and deserialization:

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);
    }
}

提示

请求和响应名称不存在于通过网络传输的序列化有效负载中。The request and response names aren't present in the serialized payloads transmitted over the wire.

模型 ID 公告Model ID announcement

若要公布模型 ID,设备必须将其包含在连接信息中:To announce the model ID, the device must include it in the connection information:

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

提示

对于模块和 IoT Edge,使用 ModuleClient 代替 ClientFor modules and IoT Edge, use ModuleClient in place of Client.

提示

这是设备可以设置模型 ID 的唯一时间,在设备连接后将无法更新。This is the only time a device can set model ID, it can't be updated after the device connects.

DPS 有效负载DPS payload

使用 设备预配服务 (DPS) 的设备可以包括 modelId 使用以下 JSON 有效负载的预配过程中要使用的。Devices using the Device Provisioning Service (DPS) can include the modelId to be used during the provisioning process using the following JSON payload.

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

实现遥测、属性和命令Implement telemetry, properties, and commands

了解 IoT 即插即用模型中的组件中所述,设备构建者必须决定是否要使用组件来描述其设备。As described in Understand components in IoT Plug and Play models, device builders must decide if they want to use components to describe their devices. 使用组件时,设备必须遵循本部分中所述的规则。When using components, devices must follow the rules described in this section.

遥测Telemetry

默认组件不需要任何特殊属性。A default component doesn't require any special property.

使用嵌套组件时,设备必须使用组件名称设置消息属性:When using nested components, devices must set a message property with the component name:

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);
}

只读属性Read-only properties

从默认组件报告属性不需要任何特殊构造:Reporting a property from the default component doesn't require any special construct:

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;
});

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

使用嵌套组件时,必须在组件名称中创建属性:When using nested components, properties must be created within the component name.:

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;
});

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

可写属性Writable properties

这些属性可以由设备设置或通过解决方案更新。These properties can be set by the device or updated by the solution. 如果解决方案更新属性,则客户端会在或中接收到作为回调的 Client 通知 ModuleClientIf the solution updates a property, the client receives a notification as a callback in the Client or ModuleClient. 若要遵循 IoT 即插即用约定,设备必须通知服务属性已成功接收。To follow the IoT Plug and Play conventions, the device must inform the service that the property was successfully received.

报告可写属性Report a writable property

当设备报告可写属性时,它必须包含 ack 约定中定义的值。When a device reports a writable property, it must include the ack values defined in the conventions.

若要从默认组件报告可写属性:To report a writable property from the default component:

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;
});

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

若要从嵌套组件报告可写属性,则克隆必须包括标记:To report a writable property from a nested component, the twin must include a marker:

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;
});

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

订阅所需属性更新Subscribe to desired property updates

服务可以更新在连接的设备上触发通知的所需属性。Services can update desired properties that trigger a notification on the connected devices. 此通知包括更新的所需属性,其中包括用于标识更新的版本号。This notification includes the updated desired properties, including the version number identifying the update. 设备必须用与报告属性相同的消息进行响应 ackDevices must respond with the same ack message as reported properties.

默认组件将查看单个属性,并 ack 使用收到的版本创建报告:A default component sees the single property and creates the reported ack with the received version:

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);
      }
    });
  });
};

设备克隆在所需的和报告的部分显示属性:The device twin shows the property in the desired and reported sections:

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

嵌套组件接收用组件名称包装的所需属性,并报告返回 ack 报告的属性:A nested component receives the desired properties wrapped with the component name, and should report back the ack reported property:

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);
      }
    });
  });
};

组件的设备克隆显示了所需的和报告的部分,如下所示:The device twin for components shows the desired and reported sections as follows:

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

命令Commands

默认组件接收服务调用的命令名称。A default component receives the command name as it was invoked by the service.

嵌套组件接收以组件名称和分隔符为前缀的命令名称 *A nested component receives the command name prefixed with the component name and the * separator.

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);

请求和响应负载Request and response payloads

命令使用类型来定义其请求和响应负载。Commands use types to define their request and response payloads. 设备必须反序列化传入的输入参数并序列化响应。A device must deserialize the incoming input parameter and serialize the response. 下面的示例演示如何实现具有负载中定义的复杂类型的命令:The following example shows how to implement a command with complex types defined in the payloads:

{
  "@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"
        }
      ]
    }
  }
}

下面的代码段演示了设备如何实现此命令定义,其中包括用于启用序列化和反序列化的类型:The following code snippets show how a device implements this command definition, including the types used to enable serialization and deserialization:

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;
  }
};

提示

请求和响应名称不存在于通过网络传输的序列化有效负载中。The request and response names aren't present in the serialized payloads transmitted over the wire.

模型 ID 公告Model ID announcement

若要公布模型 ID,设备必须将其包含在连接信息中:To announce the model ID, the device must include it in the connection information:

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,
)

提示

对于模块和 IoT Edge,使用 IoTHubModuleClient 代替 IoTHubDeviceClientFor modules and IoT Edge, use IoTHubModuleClient in place of IoTHubDeviceClient.

提示

这是设备可以设置模型 ID 的唯一时间,在设备连接后将无法更新。This is the only time a device can set model ID, it can't be updated after the device connects.

DPS 有效负载DPS payload

使用 设备预配服务 (DPS) 的设备可以包括 modelId 使用以下 JSON 有效负载的预配过程中要使用的。Devices using the Device Provisioning Service (DPS) can include the modelId to be used during the provisioning process using the following JSON payload.

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

实现遥测、属性和命令Implement telemetry, properties, and commands

了解 IoT 即插即用模型中的组件中所述,设备构建者必须决定是否要使用组件来描述其设备。As described in Understand components in IoT Plug and Play models, device builders must decide if they want to use components to describe their devices. 使用组件时,设备必须遵循本部分中所述的规则。When using components, devices must follow the rules described in this section.

遥测Telemetry

默认组件不需要任何特殊属性。A default component doesn't require any special property.

使用嵌套组件时,设备必须使用组件名称设置消息属性:When using nested components, devices must set a message property with the component name:

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)
}

只读属性Read-only properties

从默认组件报告属性不需要任何特殊构造:Reporting a property from the default component doesn't require any special construct:

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

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

使用嵌套组件时,必须在组件名称中创建属性:When using nested components, properties must be created within the component name:

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)

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

可写属性Writable properties

这些属性可以由设备设置或通过解决方案更新。These properties can be set by the device or updated by the solution. 如果解决方案更新属性,则客户端会在或中接收到作为回调的 IoTHubDeviceClient 通知 IoTHubModuleClientIf the solution updates a property, the client receives a notification as a callback in the IoTHubDeviceClient or IoTHubModuleClient. 若要遵循 IoT 即插即用约定,设备必须通知服务属性已成功接收。To follow the IoT Plug and Play conventions, the device must inform the service that the property was successfully received.

报告可写属性Report a writable property

当设备报告可写属性时,它必须包含 ack 约定中定义的值。When a device reports a writable property, it must include the ack values defined in the conventions.

若要从默认组件报告可写属性:To report a writable property from the default component:

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)

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

若要从嵌套组件报告可写属性,则克隆必须包括标记:To report a writable property from a nested component, the twin must include a marker:

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)

用下一个报告的属性更新设备克隆:The device twin is updated with the next reported property:

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

订阅所需属性更新Subscribe to desired property updates

服务可以更新在连接的设备上触发通知的所需属性。Services can update desired properties that trigger a notification on the connected devices. 此通知包括更新的所需属性,其中包括用于标识更新的版本号。This notification includes the updated desired properties, including the version number identifying the update. 设备必须用与报告属性相同的消息进行响应 ackDevices must respond with the same ack message as reported properties.

默认组件将查看单个属性,并 ack 使用收到的版本创建报告:A default component sees the single property and creates the reported ack with the received version:

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)

设备克隆在所需的和报告的部分显示属性:The device twin shows the property in the desired and reported sections:

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

嵌套组件接收用组件名称包装的所需属性,并报告返回 ack 报告的属性:A nested component receives the desired properties wrapped with the component name, and should report back the ack reported property:

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)

组件的设备克隆显示了所需的和报告的部分,如下所示:The device twin for components shows the desired and reported sections as follows:

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

命令Commands

默认组件接收服务调用的命令名称。A default component receives the command name as it was invoked by the service.

嵌套组件接收以组件名称和分隔符为前缀的命令名称 *A nested component receives the command name prefixed with the component name and the * separator.

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

请求和响应负载Request and response payloads

命令使用类型来定义其请求和响应负载。Commands use types to define their request and response payloads. 设备必须反序列化传入的输入参数并序列化响应。A device must deserialize the incoming input parameter and serialize the response. 下面的示例演示如何实现具有负载中定义的复杂类型的命令:The following example shows how to implement a command with complex types defined in the payloads:

{
  "@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"
        }
      ]
    }
  }
}

下面的代码段演示了设备如何实现此命令定义,其中包括用于启用序列化和反序列化的类型:The following code snippets show how a device implements this command definition, including the types used to enable serialization and deserialization:

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

提示

请求和响应名称不存在于通过网络传输的序列化有效负载中。The request and response names aren't present in the serialized payloads transmitted over the wire.

后续步骤Next steps

现在,你已了解 IoT 即插即用设备开发,以下是一些其他资源:Now that you've learned about IoT Plug and Play device development, here are some additional resources: