教程:使用 Linux 容器开发 C IoT Edge 模块

适用于:“是”图标 IoT Edge 1.1 “是”图标 IoT Edge 1.2

使用 Visual Studio Code 开发 C 代码并将其部署到运行 Azure IoT Edge 的设备。

可以使用 IoT Edge 模块部署代码,以直接将业务逻辑实现到 IoT Edge 设备。 本教程详细介绍如何创建并部署用于筛选传感器数据的 IoT Edge 模块。 本教程介绍如何执行下列操作:

  • 使用 Visual Studio Code 在 C 中创建 IoT Edge 模块
  • 使用 Visual Studio Code 和 Docker 创建 Docker 映像并将其发布到容器注册表
  • 将模块部署到 IoT Edge 设备
  • 查看生成的数据

在本教程中创建的 IoT Edge 模块可以筛选由设备生成的温度数据。 它只在温度高于指定阈值的情况下,向上游发送消息。 在边缘进行的此类分析适用于减少传递到云中和存储在云中的数据量。

如果没有 Azure 订阅,可在开始前创建一个试用帐户

先决条件

本教程演示如何在 Visual Studio Code 中使用 C 开发模块,以及如何将其部署到 IoT Edge 设备。 如果要使用 Windows 容器开发模块,请改为参阅使用 Windows 容器开发 C IoT Edge 模块

通过下表了解使用 Linux 容器开发和部署 C 模块的选项:

C Visual Studio Code Visual Studio
Linux AMD64 使用 VS Code 开发 Linux AMD64 上的 C 模块 使用 VS 开发 Linux AMD64 上的 C 模块
Linux ARM32 使用 VS Code 开发 Linux ARM32 上的 C 模块 使用 VS 开发 Linux ARM32 上的 C 模块

在开始学习本教程之前,应已完成介绍如何设置用于开发 Linux 容器的开发环境的上一教程:使用 Linux 容器开发 IoT Edge 模块。 完成该教程后,已应准备好以下必备组件:

若要开发以 C 编写的 IoT Edge 模块,请在开发计算机上安装下述额外的必备组件:

本教程不需要安装 Azure IoT C SDK,但它可提供有用的功能,如 IntelliSense 和读取程序定义。 有关安装的信息,请参阅 Azure IoT C SDK 和库

创建模块项目

以下步骤使用 Visual Studio Code 和 Azure IoT Edge Tools 扩展创建适用于 C 的 IoT Edge 模块项目。 创建项目模板后,请添加新代码,使模块根据其报告属性筛选出消息。

创建新项目

创建可以使用自己的代码进行自定义的 C 解决方案模板。

  1. 选择“视图” > “命令面板”,打开 VS Code 命令面板。

  2. 在命令面板中,键入并运行“Azure: 登录”命令,然后按说明登录 Azure 帐户。 如果已登录,则可跳过此步骤。

  3. 在命令面板中,键入并运行“Azure IoT Edge: 新建 IoT Edge 解决方案”命令。 按命令面板中的提示创建解决方案。

    字段 Value
    选择文件夹 在适用于 VS Code 的开发计算机上选择用于创建解决方案文件的位置。
    提供解决方案名称 输入解决方案的描述性名称,或者接受默认的 EdgeSolution
    选择模块模板 选择“C 模块”。
    提供模块名称 将模块命名为 CModule
    为模块提供 Docker 映像存储库 映像存储库包含容器注册表的名称和容器映像的名称。 容器映像是基于你在上一步中提供的名称预先填充的。 将 localhost:5000 替换为 Azure 容器注册表中的“登录服务器”值 。 可以在 Azure 门户的容器注册表的“概述”页中检索登录服务器。

    最终的映像存储库看起来类似于 <registry name>.azurecr.cn/cmodule。

    提供 Docker 映像存储库

添加注册表凭据

环境文件存储容器注册表的凭据,并将其与 IoT Edge 运行时共享。 此运行时需要这些凭据才能将专用映像拉取到 IoT Edge 设备中。

IoT Edge 扩展尝试从 Azure 提取容器注册表凭据,并将其填充到环境文件中。 请检查是否已包含你的凭据。 如果未包含,现在请添加这些凭据:

  1. 在 VS Code 资源管理器中,打开 .env 文件。
  2. 使用从 Azure 容器注册表复制的 usernamepassword 值更新相关字段。
  3. 保存此文件。

备注

本教程使用 Azure 容器注册表的管理员登录凭据,这对于开发和测试方案非常方便。 为生产方案做好准备后,建议使用最低权限身份验证选项(如服务主体)。 有关详细信息,请参阅管理容器注册表的访问权限

选择目标体系结构

目前,Visual Studio Code 可以开发适用于 Linux AMD64 和 Linux ARM32v7 设备的 C 模块。 需要选择面向每个解决方案的体系结构,因为每种体系结构类型的容器的生成和运行方式均不相同。 默认设置为 Linux AMD64。

  1. 打开命令面板并搜索“Azure IoT Edge: Set Default Target Platform for Edge Solution”,或选择窗口底部侧栏中的快捷方式图标。

  2. 在命令面板中,从选项列表中选择目标体系结构。 本教程将使用 Ubuntu 虚拟机作为 IoT Edge 设备,因此将保留默认设置 amd64

使用自定义代码更新模块

默认模块代码接收输入队列中的消息,然后通过输出队列传递这些消息。 让我们添加一些附加的代码,使模块在将消息转发到 IoT 中心之前,先在边缘上对其进行处理。 更新模块,使其分析每条消息中的温度数据,并仅在温度超过特定的阈值时,才将消息发送到 IoT 中心。

  1. 在此场景中,来自传感器的数据采用 JSON 格式。 若要筛选 JSON 格式的消息,请导入用于 C 的 JSON 库。本教程使用 Parson。

    1. 下载 Parson GitHub 存储库。 将 parson.cparson.h 文件复制到 CModule 文件夹中。

    2. 打开“模块” > “CModule” > “CMakeLists.txt”。 在文件顶部,导入名为 my_parson 的充当库的 Parson 文件。

      add_library(my_parson
          parson.c
          parson.h
      )
      
    3. my_parson 添加到 CMakeLists.txt 的 target_link_libraries 函数中的库列表。

    4. 保存 CMakeLists.txt 文件。

    5. 打开“模块” > “CModule” > “main.c”。 在 include 语句的列表底部,添加一个新的语句,以便包括适用于 JSON 支持的 parson.h

      #include "parson.h"
      
  2. main.c 文件中,在 include 节后面添加名为 temperatureThreshold 的全局变量。 此变量设置一个值,若要向 IoT 中心发送数据,测量的温度必须超出该值。

    static double temperatureThreshold = 25;
    
  3. 在 main.c 中找到 CreateMessageInstance 函数。 将内部的 if-else 语句替换为以下代码,以添加几行功能:

        if ((messageInstance->messageHandle = IoTHubMessage_Clone(message)) == NULL)
        {
            free(messageInstance);
            messageInstance = NULL;
        }
        else
        {
            messageInstance->messageTrackingId = messagesReceivedByInput1Queue;
            MAP_HANDLE propMap = IoTHubMessage_Properties(messageInstance->messageHandle);
            if (Map_AddOrUpdate(propMap, "MessageType", "Alert") != MAP_OK)
            {
               printf("ERROR: Map_AddOrUpdate Failed!\r\n");
            }
        }
    

    else 语句中的新代码行将一个新属性添加到消息,用于将消息标记为警报。 此代码将所有消息标记为警报,因为仅当报告了较高的温度时,我们要添加的功能才会向 IoT 中心发送消息。

  4. 将整个 InputQueue1Callback 函数替换为以下代码。 此函数实现实际的消息传送筛选器。 收到消息后,它会检查报告的温度是否超过阈值。 如果超过了阈值,则通过其输出队列转发消息。 如果未超过阈值,则忽略消息。

    static unsigned char *bytearray_to_str(const unsigned char *buffer, size_t len)
    {
        unsigned char *ret = (unsigned char *)malloc(len + 1);
        memcpy(ret, buffer, len);
        ret[len] = '\0';
        return ret;
    }
    
    static IOTHUBMESSAGE_DISPOSITION_RESULT InputQueue1Callback(IOTHUB_MESSAGE_HANDLE message, void* userContextCallback)
    {
        IOTHUBMESSAGE_DISPOSITION_RESULT result;
        IOTHUB_CLIENT_RESULT clientResult;
        IOTHUB_MODULE_CLIENT_LL_HANDLE iotHubModuleClientHandle = (IOTHUB_MODULE_CLIENT_LL_HANDLE)userContextCallback;
    
        unsigned const char* messageBody;
        size_t contentSize;
    
        if (IoTHubMessage_GetByteArray(message, &messageBody, &contentSize) == IOTHUB_MESSAGE_OK)
        {
            messageBody = bytearray_to_str(messageBody, contentSize);
        } else
        {
            messageBody = "<null>";
        }
    
        printf("Received Message [%zu]\r\n Data: [%s]\r\n",
                messagesReceivedByInput1Queue, messageBody);
    
        // Check if the message reports temperatures higher than the threshold
        JSON_Value *root_value = json_parse_string(messageBody);
        JSON_Object *root_object = json_value_get_object(root_value);
        double temperature;
        if (json_object_dotget_value(root_object, "machine.temperature") != NULL && (temperature = json_object_dotget_number(root_object, "machine.temperature")) > temperatureThreshold)
        {
            printf("Machine temperature %f exceeds threshold %f\r\n", temperature, temperatureThreshold);
            // This message should be sent to next stop in the pipeline, namely "output1".  What happens at "outpu1" is determined
            // by the configuration of the Edge routing table setup.
            MESSAGE_INSTANCE *messageInstance = CreateMessageInstance(message);
            if (NULL == messageInstance)
            {
                result = IOTHUBMESSAGE_ABANDONED;
            }
            else
            {
                printf("Sending message (%zu) to the next stage in pipeline\n", messagesReceivedByInput1Queue);
    
                clientResult = IoTHubModuleClient_LL_SendEventToOutputAsync(iotHubModuleClientHandle, messageInstance->messageHandle, "output1", SendConfirmationCallback, (void *)messageInstance);
                if (clientResult != IOTHUB_CLIENT_OK)
                {
                    IoTHubMessage_Destroy(messageInstance->messageHandle);
                    free(messageInstance);
                    printf("IoTHubModuleClient_LL_SendEventToOutputAsync failed on sending msg#=%zu, err=%d\n", messagesReceivedByInput1Queue, clientResult);
                    result = IOTHUBMESSAGE_ABANDONED;
                }
                else
                {
                    result = IOTHUBMESSAGE_ACCEPTED;
                }
            }
        }
        else
        {
            printf("Not sending message (%zu) to the next stage in pipeline.\r\n", messagesReceivedByInput1Queue);
            result = IOTHUBMESSAGE_ACCEPTED;
        }
    
        messagesReceivedByInput1Queue++;
        return result;
    }
    
  5. 添加 moduleTwinCallback 函数。 此方法从孪生模块接收所需属性的更新,然后更新 temperatureThreshold 变量,使之匹配。 所有模块都有自己的孪生模块,因此可以直接从云配置在模块中运行的代码。

    static void moduleTwinCallback(DEVICE_TWIN_UPDATE_STATE update_state, const unsigned char* payLoad, size_t size, void* userContextCallback)
    {
        printf("\r\nTwin callback called with (state=%s, size=%zu):\r\n%s\r\n",
            MU_ENUM_TO_STRING(DEVICE_TWIN_UPDATE_STATE, update_state), size, payLoad);
        JSON_Value *root_value = json_parse_string(payLoad);
        JSON_Object *root_object = json_value_get_object(root_value);
        if (json_object_dotget_value(root_object, "desired.TemperatureThreshold") != NULL) {
            temperatureThreshold = json_object_dotget_number(root_object, "desired.TemperatureThreshold");
        }
        if (json_object_get_value(root_object, "TemperatureThreshold") != NULL) {
            temperatureThreshold = json_object_get_number(root_object, "TemperatureThreshold");
        }
    }
    
  6. 找到 SetupCallbacksForModule 函数。 将该函数替换为以下代码,以添加 else if 语句来检查模块孪生是否已更新。

    static int SetupCallbacksForModule(IOTHUB_MODULE_CLIENT_LL_HANDLE iotHubModuleClientHandle)
    {
        int ret;
    
        if (IoTHubModuleClient_LL_SetInputMessageCallback(iotHubModuleClientHandle, "input1", InputQueue1Callback, (void*)iotHubModuleClientHandle) != IOTHUB_CLIENT_OK)
        {
            printf("ERROR: IoTHubModuleClient_LL_SetInputMessageCallback(\"input1\")..........FAILED!\r\n");
            ret = MU_FAILURE;
        }
        else if (IoTHubModuleClient_LL_SetModuleTwinCallback(iotHubModuleClientHandle, moduleTwinCallback, (void*)iotHubModuleClientHandle) != IOTHUB_CLIENT_OK)
        {
            printf("ERROR: IoTHubModuleClient_LL_SetModuleTwinCallback(default)..........FAILED!\r\n");
            ret = MU_FAILURE;
        }
        else
        {
            ret = 0;
        }
    
        return ret;
    }
    
  7. 保存 main.c 文件。

  8. 在 VS Code 资源管理器的 IoT Edge 解决方案工作区中打开 deployment.template.json 文件。

  9. 将 CModule 模块孪生添加到部署清单。 在 moduleContent 节底部 $edgeHub 模块孪生后插入以下 JSON 内容:

    "CModule": {
        "properties.desired":{
            "TemperatureThreshold":25
        }
    }
    

    将 CModule 孪生添加到部署模板

  10. 保存 deployment.template.json 文件。

生成并推送模块

在上一部分,你已经创建了一个 IoT Edge 解决方案并将代码添加到了 CModule,该函数会筛选出其中报告的计算机温度处于可接受限制范围内的消息。 现在需将解决方案生成为容器映像并将其推送到容器注册表。

  1. 选择“视图” > “终端”打开 VS Code 终端。

  2. 在终端中输入以下命令,以登录到 Docker。 使用 Azure 容器注册表中的用户名、密码和登录服务器登录。 可以在 Azure 门户中从注册表的“访问密钥”部分检索这些值。

    docker login -u <ACR username> -p <ACR password> <ACR login server>
    

    可能会出现一条安全警告,其中建议使用 --password-stdin。 这条最佳做法是针对生产场景建议的,这超出了本教程的范畴。 有关详细信息,请参阅 docker login 参考。

  3. 在 VS Code 资源管理器中右键单击“deployment.template.json”文件,然后选择“生成并推送 IoT Edge 解决方案”。

    “生成并推送”命令会启动三项操作。 首先,它在解决方案中创建名为 config 的新文件夹,用于保存基于部署模板和其他解决方案文件中的信息生成的完整部署清单。 其次,它会运行 docker build,以基于目标体系结构的相应 dockerfile 生成容器映像。 然后,它会运行 docker push,以将映像存储库推送到容器注册表。

    此过程在首次运行时可能需要花费几分钟时间,但下一次运行这些命令时可以更快地完成。

将模块部署到设备

使用 Visual Studio Code 资源管理器和 Azure IoT Tools 扩展将模块项目部署到 IoT Edge 设备。 你已经为方案准备了部署清单,即 config 文件夹中的 deployment.amd64.json 文件。 现在需要做的就是选择一个设备来接收部署。

请确保 IoT Edge 设备已启动并正在运行。

  1. 在 Visual Studio Code 资源管理器中的“Azure IoT 中心”部分下,展开“设备”可查看 IoT 设备的列表。

  2. 右键单击 IoT Edge 设备的名称,然后选择“为单个设备创建部署”。

  3. 选择 config 文件夹中的 deployment.amd64.json 文件,然后单击“选择 Edge 部署清单”。 不要使用 deployment.template.json 文件。

  4. 在设备下,展开“模块”可查看已部署的正在运行的模块的列表。 单击“刷新”按钮。 此时应看到新的 CModuleSimulatedTemperatureSensor 模块以及 $edgeAgent$edgeHub 一起运行。

    启动模块可能需要数分钟时间。 IoT Edge 运行时需要接收其新部署清单、从容器运行时提取模块映像,然后启动每个新模块。

查看生成的数据

将部署清单应用到 IoT Edge 设备以后,设备上的 IoT Edge 运行时就会收集新的部署信息并开始在其上执行操作。 在设备上运行的未包括在部署清单中的任何模块都会停止。 设备中缺失的任何模块都会启动。

  1. 在 Visual Studio Code 资源管理器中右键单击 IoT Edge 设备的名称,选择“开始监视内置事件终结点”。

  2. 查看抵达 IoT 中心的消息。 消息可能需要在一段时间后才会抵达,因为 IoT Edge 设备必须接收其新部署并启动所有模块。 然后,在发送消息之前我们对 CModule 代码所做的更改需等到机器温度达到 25 度才会生效。 IoT 中心还会将消息类型“警报”添加到达到该温度阈值的任何消息。

    查看抵达 IoT 中心的消息

编辑模块孪生

我们已使用部署清单中的 CModule 模块孪生将温度阈值设置为 25 度。 可以使用模块孪生来更改功能,而无需更新模块代码。

  1. 在 Visual Studio Code 中,展开 IoT Edge 设备下的详细信息以查看正在运行的模块。

  2. 右键单击“CModule”并选择“编辑模块孪生”。

  3. 在所需属性中找到 TemperatureThreshold。 将其值更改为比上次报告的温度高出 5 到 10 度的新温度。

  4. 保存模块孪生文件。

  5. 右键单击模块孪生编辑窗格中的任意位置,然后选择“更新模块孪生”。

  6. 监视传入的设备到云消息。 应会看到,在达到新的温度阈值之前,消息会停止发送。

清理资源

如果打算继续学习下一篇建议的文章,可以保留已创建的资源和配置,以便重复使用。 还可以继续使用相同的 IoT Edge 设备作为测试设备。

否则,可以删除本文中使用的本地配置和 Azure 资源,以免产生费用。

删除 Azure 资源

删除 Azure 资源和资源组的操作不可逆。 请确保不要意外删除错误的资源组或资源。 如果在现有的包含要保留资源的资源组中创建了 IoT 中心,请只删除 IoT 中心资源本身,而不要删除资源组。

若要删除资源,请执行以下操作:

  1. 登录到 Azure 门户,然后选择“资源组”。

  2. 选择包含 IoT Edge 测试资源的资源组的名称。

  3. 查看包含在资源组中的资源的列表。 若要删除这一切,可以选择“删除资源组”。 如果只需删除部分内容,可以单击要单独删除的每个资源。

后续步骤

在本教程中,创建了 IoT Edge 模块,其中包含用于筛选 IoT Edge 设备生成的原始数据的代码。

可以继续学习后续教程,了解 Azure IoT Edge 如何帮助你部署 Azure 云服务,以在边缘位置处理和分析数据。

自定义视觉服务