你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
IoT 即插即用设备开发人员指南项目02/06/2024借助 IoT 即插即用,你可以生成将功能播发到 Azure IoT 应用程序的 IoT 设备。 当客户将 IoT 即插即用设备连接到已启用 IoT 即插即用的应用程序(如 IoT Central)时,无需手动配置。
可以使用模块或 IoT Edge 模块直接实现 IoT 设备。
本指南介绍了创建遵循 IoT 即插即用约定的设备、模块或 IoT Edge 模块所需的基本步骤。
若要生成 IoT 即插即用设备、模块或 IoT Edge 模块,请按以下步骤操作:
确保你的设备使用 MQTT 或 MQTT over WebSockets 协议连接到 Azure IoT 中心。创建数字孪生定义语言 (DTDL) 模型来描述你的设备。 若要了解详细信息,请参阅了解 IoT 即插即用模型中的组件。更新设备或模块,以宣布 model-id 作为设备连接的一部分。按照 IoT 即插即用约定实现遥测、属性和命令在设备或模块实现就绪后,使用 Azure IoT 资源管理器来验证设备是否遵循 IoT 即插即用约定。
模型 ID 公告若要公布模型 ID,设备必须将其包含在连接信息中:
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_LL。
提示
设备只有在这时能够设置模型 ID,设备连接后将无法更新模型 ID。
DPS 有效负载使用设备预配服务 (DPS) 的设备可以包含要在使用以下 JSON 有效负载的预配过程中使用的 modelId:
{ "modelId" : "dtmi:com:example:Thermostat;1"}使用组件如了解 IoT 即插即用模型中的组件中所述,你应该决定是否要使用组件来描述设备。 使用组件时,设备必须遵循以下部分所述规则:
遥测默认组件不需要向遥测消息添加任何特殊属性。
使用嵌套组件时,设备必须使用组件名称设置消息属性:
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);只读属性从默认组件报告属性不需要任何特殊构造:
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));使用以下报告属性更新设备孪生:
{ "reported": { "maxTempSinceLastReboot" : 38.7 }}使用嵌套组件时,请在组件名称中创建属性并包括一个标记:
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);使用以下报告属性更新设备孪生:
{ "reported": { "thermostat1" : { "__t" : "c", "maxTemperature" : 38.7 } }}可写属性这些属性可以由设备设置或通过后端应用程序进行更新。 如果后端应用程序更新属性,客户端会接收到通知(即 DeviceClient 或 ModuleClient 中的回调)。 若要遵循 IoT 即插即用约定,设备必须通知服务已成功接收属性。
如果属性类型为 Object,则服务必须将完整的对象发送到设备,即使它只更新对象字段的子集,也是如此。 设备发送的确认也可以是一个完整的对象。
报告可写属性设备报告可写属性时,必须包含约定中定义的 ack 值。
从默认组件报告可写属性:
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);使用以下报告属性更新设备孪生:
{ "reported": { "targetTemperature": { "value": 23.2, "ac": 200, "av": 3, "ad": "Successfully updated target temperature" } }}若要从嵌套组件报告可写属性,孪生必须包括标记:
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);使用以下报告属性更新设备孪生:
{ "reported": { "thermostat1": { "__t" : "c", "targetTemperature": { "value": 23.2, "ac": 200, "av": 3, "ad": "complete" } } }}订阅所需的属性更新服务可以更新所需的属性,从而在连接的设备上触发通知。 此通知包括更新的所需属性(包括用于标识更新的版本号)。 设备必须在发送回服务的 ack 消息中包含此版本号。
默认组件会查看单个属性,并使用收到的版本创建报告的 ack:
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))嵌套组件的设备孪生显示了所需部分和报告部分,如下所示:
{ "desired" : { "targetTemperature": 23.2, "$version" : 3 }, "reported": { "targetTemperature": { "value": 23.2, "ac": 200, "av": 3, "ad": "Successfully updated target temperature" } }}嵌套组件接收用组件名称包装的所需属性,并且应报告 ack 报告的属性:
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);}嵌套组件的设备孪生显示了所需部分和报告部分,如下所示:
{ "desired" : { "thermostat1" : { "__t" : "c", "targetTemperature": 23.2, } "$version" : 3 }, "reported": { "thermostat1" : { "__t" : "c", "targetTemperature": { "value": 23.2, "ac": 200, "av": 3, "ad": "complete" } } }}命令默认组件接收服务调用的命令名称。
嵌套组件接收带有组件名称和 * 分隔符前缀的命令名称。
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);请求和响应有效负载命令使用类型定义其请求和响应有效负载。 设备必须对传入的输入参数进行反序列化,并对响应进行序列化。
下面的示例演示如何实现在有效负载中定义了复杂类型的命令:
{ "@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" } ] } }}下面的代码片段演示了设备如何实现此命令定义,包括用于启用序列化和反序列化的类型:
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;}提示
请求和响应名称不存在于通过网络传输的序列化有效负载中。
SDK