Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
IoT Plug and Play le permite crear dispositivos IoT que anuncian sus funcionalidades en aplicaciones de Azure IoT. Los dispositivos IoT Plug and Play no requieren configuración manual cuando un cliente los conecta a aplicaciones habilitadas para IoT Plug and Play, como IoT Central.
Puede implementar un dispositivo IoT directamente mediante módulos o mediante módulos de IoT Edge.
En esta guía se describen los pasos básicos necesarios para crear un dispositivo, un módulo o un módulo de IoT Edge que siga las convenciones de IoT Plug and Play.
Para construir un dispositivo IoT Plug and Play, un módulo o un módulo IoT Edge, siga estos pasos:
- Asegúrese de que el dispositivo usa el protocolo MQTT o MQTT a través de WebSockets para conectarse a Azure IoT Hub.
- Cree un modelo de Lenguaje de definición de Digital Twins (DTDL) para describir el dispositivo. Para más información, consulte Descripción de los componentes de los modelos de IoT Plug and Play.
- Actualice su dispositivo o módulo para anunciar el
model-idcomo parte de la conexión. - Implementar telemetría, propiedades y comandos que siguen las convenciones de IoT Plug and Play
Una vez que el dispositivo o la implementación del módulo estén listos, use azure IoT Explorer para validar que el dispositivo sigue las convenciones de IoT Plug and Play.
Código de ejemplo
Puede encontrar el código de ejemplo para muchas de las construcciones de IoT Plug and Play descritas en este artículo en el repositorio de GitHub de los SDKs C y bibliotecas de Azure IoT.
Anuncio de identificación del modelo
Para anunciar el identificador del modelo, el dispositivo debe incluirlo en la información de conexión:
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);
Sugerencia
En el caso de los módulos y IoT Edge, use IoTHubModuleClient_LL en lugar de IoTHubDeviceClient_LL.
Sugerencia
Esta es la única vez que un dispositivo puede establecer el identificador de modelo, no se puede actualizar después de que el dispositivo se conecte.
Carga útil de DPS
Los dispositivos que usan Device Provisioning Service (DPS) pueden incluir el modelId que se usará durante el proceso de aprovisionamiento mediante la siguiente carga JSON:
{
"modelId" : "dtmi:com:example:Thermostat;1"
}
Uso de componentes
Como se describe en Descripción de los componentes de los modelos de IoT Plug and Play, debe decidir si desea usar componentes para describir los dispositivos. Al usar componentes, los dispositivos deben seguir las reglas descritas en las secciones siguientes:
Telemetría
Un componente predeterminado no requiere ninguna propiedad especial agregada al mensaje de telemetría.
Al usar componentes anidados, los dispositivos deben establecer una propiedad de mensaje con el nombre del componente:
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);
Propiedades de solo lectura
La creación de informes de una propiedad del componente predeterminado no requiere ninguna construcción especial:
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));
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"maxTempSinceLastReboot" : 38.7
}
}
Al usar componentes anidados, cree propiedades en el nombre del componente e incluya un marcador:
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);
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1" : {
"__t" : "c",
"maxTemperature" : 38.7
}
}
}
Propiedades editables
Estas propiedades pueden ser establecidas por el dispositivo o actualizadas por la aplicación back-end. Si la aplicación backend actualiza un valor de configuración, el cliente recibe una notificación como callback en DeviceClient o ModuleClient. Para seguir las convenciones de IoT Plug and Play, el dispositivo debe informar al servicio de que la propiedad se recibió correctamente.
Si el tipo de propiedad es Object, el servicio debe enviar un objeto completo al dispositivo aunque solo actualice un subconjunto de los campos del objeto. La confirmación que envía el dispositivo también puede ser un objeto completo.
Notificar una propiedad grabable
Cuando un dispositivo notifica una propiedad grabable, debe incluir los ack valores definidos en las convenciones.
Para informar sobre una propiedad escribible desde el componente predeterminado:
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);
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "Successfully updated target temperature"
}
}
}
Para notificar una propiedad escribible desde un componente anidado, el twin debe tener un marcador:
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);
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1": {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
Suscribirse a las actualizaciones de propiedades deseadas
Los servicios pueden actualizar las propiedades deseadas que desencadenan una notificación en los dispositivos conectados. Esta notificación incluye las propiedades deseadas actualizadas, incluido el número de versión que identifica la actualización. Los dispositivos deben incluir este número de versión en el ack mensaje enviado de vuelta al servicio.
Un componente por defecto identifica la única propiedad y crea el componente informado ack con la versión recibida.
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))
El dispositivo gemelo de un componente anidado muestra las secciones deseadas e notificadas de la siguiente manera:
{
"desired" : {
"targetTemperature": 23.2,
"$version" : 3
},
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "Successfully updated target temperature"
}
}
}
Un componente anidado recibe las propiedades deseadas envueltas con el nombre del componente y debe notificar la ack propiedad informada:
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);
}
El dispositivo gemelo de un componente anidado muestra las secciones deseadas e notificadas de la siguiente manera:
{
"desired" : {
"thermostat1" : {
"__t" : "c",
"targetTemperature": 23.2,
}
"$version" : 3
},
"reported": {
"thermostat1" : {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
Commands
Un componente predeterminado recibe el nombre del comando tal como lo invocó el servicio.
Un componente que está anidado recibe el nombre del comando precedido por el nombre del componente y el separador *.
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);
Cargas de solicitud y respuesta
Los comandos usan tipos para definir sus cargas de solicitud y respuesta. Un dispositivo debe deserializar el parámetro de entrada entrante y serializar la respuesta.
En el ejemplo siguiente se muestra cómo implementar un comando con tipos complejos definidos en las cargas:
{
"@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"
}
]
}
}
}
Los fragmentos de código siguientes muestran cómo un dispositivo implementa esta definición de comando, incluidos los tipos usados para habilitar la serialización y la deserialización:
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;
}
Sugerencia
Los nombres de solicitud y respuesta no están presentes en las cargas serializadas transmitidas por cable.
SDKs
Los fragmentos de código de este artículo se basan en ejemplos que usan el complemento de Middleware de Azure IoT para Eclipse ThreadX. El complemento es una capa de enlace entre Eclipse ThreadX y el SDK de Azure para Embedded C.
Los fragmentos de código de este artículo se basan en los ejemplos siguientes:
Anuncio de id. de modelo
Para anunciar el identificador del modelo, el dispositivo debe incluirlo en la información de conexión:
#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);
Sugerencia
Esta es la única vez que un dispositivo puede establecer el identificador de modelo, no se puede actualizar después de que el dispositivo se conecte.
Carga de DPS
Los dispositivos que utilizan Device Provisioning Service (DPS) pueden incluir el modelId para usar durante el proceso de aprovisionamiento usando la siguiente carga JSON:
{
"modelId" : "dtmi:com:example:Thermostat;1"
}
En el ejemplo se usa el código siguiente para enviar esta carga:
#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);
Utiliza componentes
Como se describe en Descripción de los componentes de los modelos de IoT Plug and Play, debe decidir si desea usar componentes para describir los dispositivos. Al usar componentes, los dispositivos deben seguir las reglas descritas en las secciones siguientes. Para simplificar el trabajo con las convenciones de IoT Plug and Play para los componentes, los ejemplos usan las funciones auxiliares en nx_azure_iot_hub_client.h.
Telemetría
Un componente predeterminado no requiere ninguna propiedad especial agregada al mensaje de telemetría.
Cuando se usan componentes anidados, los dispositivos deben establecer una propiedad de mensaje con el nombre del componente. En el fragmento de código siguiente, component_name_ptr es el nombre de un componente como thermostat1. La función auxiliar nx_azure_iot_pnp_helper_telemetry_message_create definida en nx_azure_iot_pnp_helpers.h agrega la propiedad de mensaje con el nombre del componente.
#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);
}
Propiedades de solo lectura
La creación de informes de una propiedad del componente predeterminado no requiere ninguna construcción especial:
#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))))
{
// ...
}
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"maxTempSinceLastReboot" : 38.7
}
}
Cuando se usan componentes anidados, se deben crear propiedades dentro del nombre del componente e incluir un marcador. En el fragmento de código siguiente, component_name_ptr es el nombre de un componente como thermostat1. La función nx_azure_iot_pnp_helper_build_reported_property auxiliar definida en nx_azure_iot_pnp_helpers.h crea la propiedad notificada en el formato correcto:
#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))))
{
// ...
}
// ...
}
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1" : {
"__t" : "c",
"maxTemperature" : 38.7
}
}
}
Propiedades editables
Estas propiedades pueden ser establecidas por el dispositivo o actualizadas por la aplicación back-end. Para seguir las convenciones de IoT Plug and Play, el dispositivo debe informar al servicio de que la propiedad se recibió correctamente.
Notificar una propiedad escribible
Cuando un dispositivo notifica una propiedad grabable, debe incluir los ack valores definidos en las convenciones.
Para notificar una propiedad grabable desde el componente predeterminado:
#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
// ...
}
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "success"
}
}
}
Para notificar una propiedad grabable desde un componente anidado, el gemelo debe incluir un marcador y las propiedades deben crearse dentro del nombre del componente. En el fragmento de código siguiente, component_name_ptr es el nombre de un componente como thermostat1. La función nx_azure_iot_pnp_helper_build_reported_property_with_status auxiliar definida en nx_azure_iot_pnp_helpers.h crea la carga de la propiedad notificada:
#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
{
// ...
}
// ...
}
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1": {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "success"
}
}
}
}
Suscribirse a las actualizaciones de propiedades deseadas
Los servicios pueden actualizar las propiedades deseadas que desencadenan una notificación en los dispositivos conectados. Esta notificación incluye las propiedades deseadas actualizadas y el número de versión que identifica la actualización. Los dispositivos deben incluir este número de versión en el ack mensaje enviado de vuelta al servicio.
Un componente predeterminado detecta la propiedad única y crea el ack reportado con la versión recibida.
#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(©_json_reader,
(UCHAR *)desired_version_property_name,
sizeof(desired_version_property_name) - 1) ||
nx_azure_iot_json_reader_token_int32_get(©_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);
// ...
}
Un componente anidado recibe las propiedades deseadas acompañadas del nombre del componente y crea el informado ack con la versión recibida.
#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);
// ...
}
El dispositivo gemelo de un componente anidado muestra las secciones deseadas e notificadas de la siguiente manera:
{
"desired" : {
"thermostat1" : {
"__t" : "c",
"targetTemperature": 23.2,
}
"$version" : 3
},
"reported": {
"thermostat1" : {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "success"
}
}
}
}
Commands
Un componente predeterminado recibe el nombre del comando tal como lo invocó el servicio.
Un componente anidado recibe el nombre del comando prefijado con el nombre del componente y con el separador *. En el fragmento de código siguiente, la función nx_azure_iot_pnp_helper_command_name_parse auxiliar definida en nx_azure_iot_pnp_helpers.h analiza el nombre del componente y el nombre del comando del mensaje que recibe el dispositivo del servicio:
#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;
}
// ...
}
}
Cargas de solicitud y respuesta
Los comandos usan tipos para definir sus cargas de solicitud y respuesta. Un dispositivo debe deserializar el parámetro de entrada entrante y serializar la respuesta.
En el ejemplo siguiente se muestra cómo implementar un comando con tipos complejos definidos en las cargas:
{
"@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"
}
]
}
}
}
Los fragmentos de código siguientes muestran cómo un dispositivo implementa esta definición de comando, incluidos los tipos usados para habilitar la serialización y la deserialización:
#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);
}
Sugerencia
Los nombres de solicitud y respuesta no están presentes en las cargas útiles serializadas transmitidas por la red.
Código de ejemplo
Puede encontrar el código de ejemplo para muchas de las construcciones de IoT Plug and Play descritas en este artículo en el repositorio de GitHub del SDK de Microsoft Azure IoT para .NET .
Anuncio de ID de modelo
Para anunciar el identificador del modelo, el dispositivo debe incluirlo en la información de conexión:
DeviceClient.CreateFromConnectionString(
connectionString,
TransportType.Mqtt,
new ClientOptions() { ModelId = modelId })
La nueva ClientOptions sobrecarga está disponible en todos los DeviceClient métodos usados para inicializar una conexión.
Sugerencia
En el caso de los módulos y IoT Edge, use ModuleClient en lugar de DeviceClient.
Sugerencia
Esta es la única vez que un dispositivo puede establecer el identificador de modelo, no se puede actualizar después de que el dispositivo se conecte.
Carga de Datos del Sistema DPS
Los dispositivos que utilizan el Device Provisioning Service (DPS) pueden incluir el modelId que se utilizará durante el proceso de aprovisionamiento mediante la siguiente carga JSON:
{
"modelId" : "dtmi:com:example:Thermostat;1"
}
Utilice componentes
Como se describe en Descripción de los componentes de los modelos de IoT Plug and Play, debe decidir si desea usar componentes para describir los dispositivos. Al usar componentes, los dispositivos deben seguir las reglas descritas en las secciones siguientes.
Telemetría
Un componente predeterminado no requiere ninguna propiedad especial agregada al mensaje de telemetría.
Al usar componentes anidados, los dispositivos deben establecer una propiedad de mensaje con el nombre del componente:
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);
}
Propiedades de solo lectura
La creación de informes de una propiedad del componente predeterminado no requiere ninguna construcción especial:
TwinCollection reportedProperties = new TwinCollection();
reportedProperties["maxTemperature"] = 38.7;
await client.UpdateReportedPropertiesAsync(reportedProperties);
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"maxTemperature" : 38.7
}
}
Al usar componentes anidados, cree propiedades en el nombre del componente e incluya un marcador:
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);
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1" : {
"__t" : "c",
"maxTemperature" : 38.7
}
}
}
Propiedades editables
Las propiedades se pueden establecer por el dispositivo o actualizar por la aplicación back-end. Si la aplicación de back-end actualiza una propiedad, el cliente recibe una notificación como devolución de llamada en DeviceClient o ModuleClient. Para seguir las convenciones de IoT Plug and Play, el dispositivo debe informar al servicio que la propiedad se recibió correctamente.
Si el tipo de propiedad es Object, el servicio debe enviar un objeto completo al dispositivo aunque solo actualice un subconjunto de los campos del objeto. La confirmación que envía el dispositivo también debe ser un objeto completo.
Notificar una propiedad escribible
Cuando un dispositivo notifica una propiedad grabable, debe incluir los ack valores definidos en las convenciones.
Para notificar una propiedad escribible desde el componente predeterminado:
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);
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
Para notificar una propiedad escribible desde un componente anidado, el gemelo debe incluir un marcador:
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);
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1": {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
Suscribirse a las actualizaciones de propiedades deseadas
Los servicios pueden actualizar las propiedades deseadas que desencadenan una notificación en los dispositivos conectados. Esta notificación incluye las propiedades deseadas actualizadas, incluido el número de versión que identifica la actualización. Los dispositivos deben incluir este número de versión en el ack mensaje enviado de vuelta al servicio.
Un componente predeterminado identifica la propiedad singular y crea el componente informado ack con la versión recibida:
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);
El dispositivo gemelo de un componente anidado muestra las secciones deseadas e notificadas de la siguiente manera:
{
"desired" : {
"targetTemperature": 23.2,
"$version" : 3
},
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
Un componente anidado recibe las propiedades deseadas envueltas con el nombre del componente y debe informar sobre la ack propiedad informada.
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);
El dispositivo gemelo de un componente anidado muestra las secciones deseadas e notificadas de la siguiente manera:
{
"desired" : {
"thermostat1" : {
"__t" : "c",
"targetTemperature": 23.2,
}
"$version" : 3
},
"reported": {
"thermostat1" : {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
Commands
Un componente predeterminado recibe el nombre del comando tal como lo invocó el servicio.
Un componente anidado recibe el nombre del comando prefijado con el nombre del componente y el separador *.
await client.SetMethodHandlerAsync("thermostat*reboot", (MethodRequest req, object ctx) =>
{
Console.WriteLine("REBOOT");
return Task.FromResult(new MethodResponse(200));
},
null);
Cargas de solicitud y respuesta
Los comandos usan tipos para definir sus cargas de solicitud y respuesta. Un dispositivo debe deserializar el parámetro de entrada entrante y serializar la respuesta.
En el ejemplo siguiente se muestra cómo implementar un comando con tipos complejos definidos en las cargas:
{
"@type": "Command",
"name": "start",
"request": {
"name": "startRequest",
"schema": {
"@type": "Object",
"fields": [
{
"name": "startPriority",
"schema": "integer"
},
{
"name": "startMessage",
"schema" : "string"
}
]
}
},
"response": {
"name": "startResponse",
"schema": {
"@type": "Object",
"fields": [
{
"name": "startupTime",
"schema": "integer"
},
{
"name": "startupMessage",
"schema": "string"
}
]
}
}
}
Los fragmentos de código siguientes muestran cómo un dispositivo implementa esta definición de comando, incluidos los tipos usados para habilitar la serialización y la deserialización:
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);
Sugerencia
Los nombres de solicitud y respuesta no están presentes en las cargas serializadas transmitidas a través de la conexión.
Código de ejemplo
Puede encontrar el código de ejemplo para muchas de las construcciones IoT Plug and Play descritas en este artículo en el repositorio de GitHub de Microsoft Azure IoT SDKs for Java.
Anuncio de id. de modelo
Para anunciar el identificador del modelo, el dispositivo debe incluirlo en la información de conexión:
ClientOptions options = new ClientOptions();
options.setModelId(MODEL_ID);
deviceClient = new DeviceClient(deviceConnectionString, protocol, options);
La ClientOptions sobrecarga está disponible en todos los DeviceClient métodos usados para inicializar una conexión.
Sugerencia
En el caso de los módulos y IoT Edge, use ModuleClient en lugar de DeviceClient.
Sugerencia
Esta es la única vez que un dispositivo puede establecer el identificador de modelo, no se puede actualizar después de que el dispositivo se conecte.
Carga útil de DPS
Los dispositivos que usan Device Provisioning Service (DPS) pueden incluir el modelId que se usará durante el proceso de aprovisionamiento mediante la siguiente carga JSON.
{
"modelId" : "dtmi:com:example:Thermostat;1"
}
Uso de componentes
Como se describe en Descripción de los componentes de los modelos de IoT Plug and Play, debe decidir si desea usar componentes para describir los dispositivos. Al usar componentes, los dispositivos deben seguir las reglas descritas en las secciones siguientes.
Telemetría
Un componente predeterminado no requiere ninguna propiedad especial agregada al mensaje de telemetría.
Al usar componentes anidados, el dispositivo debe establecer una propiedad de mensaje con el nombre del componente:
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);
}
Propiedades de solo lectura
La creación de informes de una propiedad del componente predeterminado no requiere ninguna construcción especial:
Property reportedProperty = new Property("maxTempSinceLastReboot", 38.7);
deviceClient.sendReportedProperties(Collections.singleton(reportedProperty));
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"maxTempSinceLastReboot" : 38.7
}
}
Al usar componentes anidados, cree propiedades en el nombre del componente e incluya un marcador:
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);
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1" : {
"__t" : "c",
"maxTemperature" : 38.7
}
}
}
Propiedades editables
Estas propiedades pueden ser configuradas por el dispositivo o actualizadas por la aplicación back-end. Si la aplicación backend actualiza una propiedad, el cliente recibe una notificación como callback en DeviceClient o ModuleClient. Para seguir las convenciones de IoT Plug and Play, el dispositivo debe informar al servicio de que la propiedad se recibió correctamente.
Si el tipo de propiedad es Object, el servicio debe enviar un objeto completo al dispositivo aunque solo actualice un subconjunto de los campos del objeto. La confirmación que envía el dispositivo también debe ser un objeto completo.
Informar sobre una propiedad escribible
Cuando un dispositivo notifica una propiedad grabable, debe incluir los ack valores definidos en las convenciones.
Para notificar una propiedad grabable desde el componente predeterminado:
@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));
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "Successfully updated target temperature"
}
}
}
Para notificar una propiedad escribible desde un componente anidado, el gemelo digital debe incluir un marcador:
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);
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1": {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
Suscribirse a las actualizaciones de propiedades deseadas
Los servicios pueden actualizar las propiedades deseadas que desencadenan una notificación en los dispositivos conectados. Esta notificación incluye las propiedades deseadas actualizadas, incluido el número de versión que identifica la actualización. Los dispositivos deben incluir este número de versión en el ack mensaje enviado de vuelta al servicio.
Un componente predeterminado percibe la sola propiedad y crea el ack reportado con la versión recibida.
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);
El dispositivo gemelo de un componente anidado muestra las secciones deseadas e notificadas de la siguiente manera:
{
"desired" : {
"targetTemperature": 23.2,
"$version" : 3
},
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "Successfully updated target temperature"
}
}
}
Un componente anidado recibe las propiedades deseadas, envueltas con el nombre del componente, y debe notificar la ack propiedad informada:
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);
El dispositivo gemelo de un componente anidado muestra las secciones deseadas e notificadas de la siguiente manera:
{
"desired" : {
"thermostat1" : {
"__t" : "c",
"targetTemperature": 23.2,
}
"$version" : 3
},
"reported": {
"thermostat1" : {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
Commands
Un componente predeterminado recibe el nombre del comando tal como lo invocó el servicio.
Un componente anidado recibe el nombre del comando precedido por el nombre del componente y el separador *.
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);
}
}
}
Cargas de solicitud y respuesta
Los comandos usan tipos para definir sus cargas de solicitud y respuesta. Un dispositivo debe deserializar el parámetro de entrada entrante y serializar la respuesta.
En el ejemplo siguiente se muestra cómo implementar un comando con tipos complejos definidos en las cargas:
{
"@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"
}
]
}
}
}
Los fragmentos de código siguientes muestran cómo un dispositivo implementa esta definición de comando, incluidos los tipos usados para habilitar la serialización y la deserialización:
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);
}
}
Sugerencia
Los nombres de solicitud y respuesta no están presentes en las cargas serializadas transmitidas vía cable.
Código de ejemplo
Puede encontrar el código de ejemplo para muchas de las construcciones de IoT Plug and Play descritas en este artículo en el repositorio de GitHub de los SDKs de IoT de Microsoft Azure para Node.js.
Anuncio de ID de modelo
Para anunciar el identificador del modelo, el dispositivo debe incluirlo en la información de conexión:
const modelIdObject = { modelId: 'dtmi:com:example:Thermostat;1' };
const client = Client.fromConnectionString(deviceConnectionString, Protocol);
await client.setOptions(modelIdObject);
await client.open();
Sugerencia
En el caso de los módulos y IoT Edge, use ModuleClient en lugar de Client.
Sugerencia
Esta es la única vez que un dispositivo puede establecer el identificador de modelo, no se puede actualizar después de que el dispositivo se conecte.
Carga de DPS
Los dispositivos que usan el Device Provisioning Service (DPS) pueden incluir el modelId para usar durante el proceso de aprovisionamiento mediante la siguiente carga JSON.
{
"modelId" : "dtmi:com:example:Thermostat;1"
}
Uso de componentes
Como se describe en Descripción de los componentes de los modelos de IoT Plug and Play, debe decidir si desea usar componentes para describir los dispositivos. Al usar componentes, los dispositivos deben seguir las reglas descritas en las secciones siguientes.
Telemetría
Un componente predeterminado no requiere ninguna propiedad especial agregada al mensaje de telemetría.
Al usar componentes anidados, los dispositivos deben establecer una propiedad de mensaje con el nombre del componente:
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);
}
Propiedades de solo lectura
La creación de informes de una propiedad del componente predeterminado no requiere ninguna construcción especial:
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;
});
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"maxTempSinceLastReboot" : 38.7
}
}
Cuando se usan componentes anidados, se deben crear propiedades dentro del nombre del componente e incluir un marcador:
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;
});
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1" : {
"__t" : "c",
"maxTempSinceLastReboot" : 38.7
}
}
}
Propiedades editables
Estas propiedades pueden ser establecidas por el dispositivo o actualizadas por la aplicación back-end. Si la aplicación en el servidor actualiza una propiedad, el cliente recibe una notificación de retorno en Client o ModuleClient. Para seguir las convenciones de IoT Plug and Play, el dispositivo debe informar al servicio que la propiedad se recibió correctamente.
Si el tipo de propiedad es Object, el servicio debe enviar un objeto completo al dispositivo aunque solo actualice un subconjunto de los campos del objeto. La confirmación que envía el dispositivo también debe ser un objeto completo.
Notificar una propiedad escribible
Cuando un dispositivo notifica una propiedad grabable, debe incluir los ack valores definidos en las convenciones.
Para notificar una propiedad grabable desde el componente predeterminado:
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;
});
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 0,
"ad": "reported default value"
}
}
}
Para notificar una propiedad escribible de un componente anidado, el gemelo digital debe incluir un marcador:
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;
});
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1": {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 0,
"ad": "complete"
}
}
}
}
Suscribirse a las actualizaciones de propiedades deseadas
Los servicios pueden actualizar las propiedades deseadas que desencadenan una notificación en los dispositivos conectados. Esta notificación incluye las propiedades deseadas actualizadas, incluido el número de versión que identifica la actualización. Los dispositivos deben incluir este número de versión en el ack mensaje enviado de vuelta al servicio.
Un componente predeterminado identifica la propiedad única y crea el informado ack con la versión recibida:
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);
}
});
});
};
El dispositivo gemelo de un componente anidado muestra las secciones deseadas e notificadas de la siguiente manera:
{
"desired" : {
"targetTemperature": 23.2,
"$version" : 3
},
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
Un componente anidado recibe las propiedades deseadas envueltas con el nombre del componente y debe informar la ack propiedad notificada:
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);
}
});
});
};
El dispositivo gemelo para los componentes muestra las secciones deseadas y notificadas de la siguiente manera:
{
"desired" : {
"thermostat1" : {
"__t" : "c",
"targetTemperature": 23.2,
}
"$version" : 3
},
"reported": {
"thermostat1" : {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
Commands
Un componente predeterminado recibe el nombre del comando tal como lo invocó el servicio.
Un componente anidado recibe el nombre del comando precedido por el nombre del componente y el separador *.
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);
Cargas de solicitud y respuesta
Los comandos usan tipos para definir sus cargas de solicitud y respuesta. Un dispositivo debe deserializar el parámetro de entrada entrante y serializar la respuesta.
En el ejemplo siguiente se muestra cómo implementar un comando con tipos complejos definidos en las cargas:
{
"@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"
}
]
}
}
}
Los fragmentos de código siguientes muestran cómo un dispositivo implementa esta definición de comando, incluidos los tipos usados para habilitar la serialización y la deserialización:
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;
}
};
Sugerencia
Los nombres de solicitud y respuesta no están presentes en las cargas serializadas transmitidas a través del cable.
Código de ejemplo
Puede encontrar el código de ejemplo para muchas de las construcciones de IoT Plug and Play descritas en este artículo en el repositorio de GitHub Microsoft Azure IoT SDKs para Python.
Anuncio de ID de modelo
Para anunciar el identificador del modelo, el dispositivo debe incluirlo en la información de conexión:
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,
)
Sugerencia
En el caso de los módulos y IoT Edge, use IoTHubModuleClient en lugar de IoTHubDeviceClient.
Sugerencia
Esta es la única vez que un dispositivo puede establecer el identificador de modelo, no se puede actualizar después de que el dispositivo se conecte.
Carga útil de DPS
Los dispositivos que usan Device Provisioning Service (DPS) pueden incluir el modelId que se usará durante el proceso de aprovisionamiento mediante la siguiente carga JSON.
{
"modelId" : "dtmi:com:example:Thermostat;1"
}
Uso de componentes
Como se describe en Descripción de los componentes de los modelos de IoT Plug and Play, debe decidir si desea usar componentes para describir los dispositivos. Al usar componentes, los dispositivos deben seguir las reglas descritas en las secciones siguientes.
Telemetría
Un componente predeterminado no requiere ninguna propiedad especial agregada al mensaje de telemetría.
Al usar componentes anidados, los dispositivos deben establecer una propiedad de mensaje con el nombre del componente:
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)
Propiedades de solo lectura
La creación de informes de una propiedad del componente predeterminado no requiere ninguna construcción especial:
await device_client.patch_twin_reported_properties({"maxTempSinceLastReboot": 38.7})
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"maxTempSinceLastReboot" : 38.7
}
}
Al usar componentes anidados, se deben crear propiedades dentro del nombre del componente e incluir un marcador:
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)
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1" : {
"__t" : "c",
"maxTempSinceLastReboot" : 38.7
}
}
}
Propiedades editables
Estas propiedades pueden ser establecidas por el dispositivo o actualizadas por la aplicación back-end. Si la aplicación de servidor actualiza una propiedad, el cliente recibe una notificación como retrollamada en IoTHubDeviceClient o IoTHubModuleClient. Para seguir las convenciones de IoT Plug and Play, el dispositivo debe informar al servicio que la propiedad se recibió exitosamente.
Si el tipo de propiedad es Object, el servicio debe enviar un objeto completo al dispositivo aunque solo actualice un subconjunto de los campos del objeto. La confirmación que envía el dispositivo también debe ser un objeto completo.
Notificar una propiedad grabable
Cuando un dispositivo notifica una propiedad grabable, debe incluir los ack valores definidos en las convenciones.
Para informar una propiedad escribible desde el componente predeterminado:
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)
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 0,
"ad": "reported default value"
}
}
}
Para notificar una propiedad grabable desde un componente anidado, el gemelo debe incluir un marcador:
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)
El dispositivo gemelo se actualiza con la siguiente propiedad notificada:
{
"reported": {
"thermostat1": {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 0,
"ad": "complete"
}
}
}
}
Suscribirse a las actualizaciones de propiedades deseadas
Los servicios pueden actualizar las propiedades deseadas que desencadenan una notificación en los dispositivos conectados. Esta notificación incluye las propiedades deseadas actualizadas, incluido el número de versión que identifica la actualización. Los dispositivos deben incluir este número de versión en el ack mensaje enviado de vuelta al servicio.
Un componente predeterminado ve una sola propiedad y crea el componente reportado ack con la versión recibida:
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)
El dispositivo gemelo de un componente anidado muestra las secciones deseadas e notificadas de la siguiente manera:
{
"desired" : {
"targetTemperature": 23.2,
"$version" : 3
},
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
Un componente anidado recibe las propiedades deseadas ajustadas con el nombre del componente y debe notificar la ack propiedad notificada:
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)
El dispositivo gemelo para los componentes muestra las secciones deseadas y notificadas de la siguiente manera:
{
"desired" : {
"thermostat1" : {
"__t" : "c",
"targetTemperature": 23.2,
}
"$version" : 3
},
"reported": {
"thermostat1" : {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
Commands
Un componente predeterminado recibe el nombre del comando tal como lo invocó el servicio.
Un componente anidado recibe el nombre del comando antepuesto con el nombre del componente y el separador *.
command_request = await device_client.receive_method_request("thermostat1*reboot")
Cargas de solicitud y respuesta
Los comandos usan tipos para definir sus cargas de solicitud y respuesta. Un dispositivo debe deserializar el parámetro de entrada entrante y serializar la respuesta.
En el ejemplo siguiente se muestra cómo implementar un comando con tipos complejos definidos en las cargas:
{
"@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"
}
]
}
}
}
Los fragmentos de código siguientes muestran cómo un dispositivo implementa esta definición de comando, incluidos los tipos usados para habilitar la serialización y la deserialización:
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
Sugerencia
Los nombres de solicitud y respuesta no están presentes en las cargas útiles serializadas transmitidas a través de la red.
Pasos siguientes
Ahora que ha aprendido sobre el desarrollo de dispositivos IoT Plug and Play, estos son otros recursos: