跳至主要内容

將你的物聯網設備連接到雲端 - Wio Terminal

在本課程的這一部分,你將把你的 Wio Terminal 連接到你的物聯網中心,以發送遙測數據並接收命令。

將你的設備連接到物聯網中心

下一步是將你的設備連接到物聯網中心。

任務 - 連接到物聯網中心

  1. 在 VS Code 中打開 soil-moisture-sensor 項目

  2. 打開 platformio.ini 文件。移除 knolleary/PubSubClient 庫依賴。這個庫是用來連接到公共 MQTT 代理的,連接到物聯網中心不需要它。

  3. 添加以下庫依賴:

    seeed-studio/Seeed Arduino RTC @ 2.0.0
    arduino-libraries/AzureIoTHub @ 1.6.0
    azure/AzureIoTUtility @ 1.6.1
    azure/AzureIoTProtocol_MQTT @ 1.6.0
    azure/AzureIoTProtocol_HTTP @ 1.6.0
    azure/AzureIoTSocket_WiFi @ 1.0.2

    Seeed Arduino RTC 庫提供了與 Wio Terminal 中的實時時鐘交互的代碼,用於跟踪時間。其餘的庫允許你的物聯網設備連接到物聯網中心。

  4. platformio.ini 文件的底部添加以下內容:

    build_flags =
    -DDONT_USE_UPLOADTOBLOB

    這設置了一個編譯器標誌,在編譯 Arduino 物聯網中心代碼時需要。

  5. 打開 config.h 頭文件。移除所有的 MQTT 設置,並添加以下設備連接字符串的常量:

    // 物聯網中心設置
    const char *CONNECTION_STRING = "<connection string>";

    <connection string> 替換為你之前複製的設備連接字符串。

  6. 連接到物聯網中心使用基於時間的令牌。這意味著物聯網設備需要知道當前時間。與 Windows、macOS 或 Linux 等操作系統不同,微控制器不會自動通過互聯網同步當前時間。這意味著你需要添加代碼從 NTP 服務器獲取當前時間。一旦獲取了時間,它可以存儲在 Wio Terminal 的實時時鐘中,允許在以後請求正確的時間,假設設備沒有斷電。添加一個名為 ntp.h 的新文件,代碼如下:

    #pragma once

    #include "DateTime.h"
    #include <time.h>
    #include "samd/NTPClientAz.h"
    #include <sys/time.h>

    static void initTime()
    {
    WiFiUDP _udp;
    time_t epochTime = (time_t)-1;
    NTPClientAz ntpClient;

    ntpClient.begin();

    while (true)
    {
    epochTime = ntpClient.getEpochTime("0.pool.ntp.org");

    if (epochTime == (time_t)-1)
    {
    Serial.println("Fetching NTP epoch time failed! Waiting 2 seconds to retry.");
    delay(2000);
    }
    else
    {
    Serial.print("Fetched NTP epoch time is: ");

    char buff[32];
    sprintf(buff, "%.f", difftime(epochTime, (time_t)0));
    Serial.println(buff);
    break;
    }
    }

    ntpClient.end();

    struct timeval tv;
    tv.tv_sec = epochTime;
    tv.tv_usec = 0;

    settimeofday(&tv, NULL);
    }

    這段代碼的細節超出了本課程的範圍。它定義了一個名為 initTime 的函數,該函數從 NTP 服務器獲取當前時間並用它來設置 Wio Terminal 的時鐘。

  7. 打開 main.cpp 文件,移除所有的 MQTT 代碼,包括 PubSubClient.h 頭文件,PubSubClient 變量的聲明,reconnectMQTTClientcreateMQTTClient 方法,以及對這些變量和方法的任何調用。這個文件應該只包含連接 WiFi,獲取土壤濕度並創建 JSON 文檔的代碼。

  8. main.cpp 文件的頂部添加以下 #include 指令,以包含物聯網中心庫和設置時間的頭文件:

    #include <AzureIoTHub.h>
    #include <AzureIoTProtocol_MQTT.h>
    #include <iothubtransportmqtt.h>
    #include "ntp.h"
  9. setup 函數的末尾添加以下調用來設置當前時間:

    initTime();
  10. 在文件頂部的包含指令下方添加以下變量聲明:

    IOTHUB_DEVICE_CLIENT_LL_HANDLE _device_ll_handle;

    這聲明了一個 IOTHUB_DEVICE_CLIENT_LL_HANDLE,即連接到物聯網中心的句柄。

  11. 在這之下,添加以下代碼:

    static void connectionStatusCallback(IOTHUB_CLIENT_CONNECTION_STATUS result, IOTHUB_CLIENT_CONNECTION_STATUS_REASON reason, void *user_context)
    {
    if (result == IOTHUB_CLIENT_CONNECTION_AUTHENTICATED)
    {
    Serial.println("The device client is connected to iothub");
    }
    else
    {
    Serial.println("The device client has been disconnected");
    }
    }

    這聲明了一個回調函數,當連接到物聯網中心的狀態改變時(例如連接或斷開連接)將被調用。狀態將被發送到串行端口。

  12. 在這之下,添加一個連接到物聯網中心的函數:

    void connectIoTHub()
    {
    IoTHub_Init();

    _device_ll_handle = IoTHubDeviceClient_LL_CreateFromConnectionString(CONNECTION_STRING, MQTT_Protocol);

    if (_device_ll_handle == NULL)
    {
    Serial.println("Failure creating Iothub device. Hint: Check your connection string.");
    return;
    }

    IoTHubDeviceClient_LL_SetConnectionStatusCallback(_device_ll_handle, connectionStatusCallback, NULL);
    }

    這段代碼初始化物聯網中心庫代碼,然後使用 config.h 頭文件中的連接字符串創建一個連接。這個連接基於 MQTT。如果連接失敗,這將被發送到串行端口 - 如果你在輸出中看到這個,請檢查連接字符串。最後設置連接狀態回調。

  13. setup 函數中,將這個函數調用添加到 initTime 調用的下方:

    connectIoTHub();
  14. 就像 MQTT 客戶端一樣,這段代碼在單個線程上運行,因此需要時間來處理由中心發送和發送到中心的消息。在 loop 函數的頂部添加以下內容來實現這一點:

    IoTHubDeviceClient_LL_DoWork(_device_ll_handle);
  15. 構建並上傳這段代碼。你將在串行監視器中看到連接:

    Connecting to WiFi..
    Connected!
    Fetched NTP epoch time is: 1619983687
    Sending telemetry {"soil_moisture":391}
    The device client is connected to iothub

    在輸出中,你可以看到 NTP 時間被獲取,然後設備客戶端連接。連接可能需要幾秒鐘,所以你可能會在設備連接時看到輸出中的土壤濕度。

    💁 你可以使用像 unixtimestamp.com 這樣的網站將 NTP 的 UNIX 時間轉換為更易讀的版本

發送遙測數據

現在你的設備已經連接,你可以將遙測數據發送到物聯網中心,而不是 MQTT 代理。

任務 - 發送遙測數據

  1. setup 函數上方添加以下函數:

    void sendTelemetry(const char *telemetry)
    {
    IOTHUB_MESSAGE_HANDLE message_handle = IoTHubMessage_CreateFromString(telemetry);
    IoTHubDeviceClient_LL_SendEventAsync(_device_ll_handle, message_handle, NULL, NULL);
    IoTHubMessage_Destroy(message_handle);
    }

    這段代碼從作為參數傳遞的字符串創建一個物聯網中心消息,將其發送到中心,然後清理消息對象。

  2. loop 函數中,在將遙測數據發送到串行端口的行之後調用這段代碼:

    sendTelemetry(telemetry.c_str());

處理命令

你的設備需要處理來自服務器代碼的命令以控制繼電器。這是作為直接方法請求發送的。

任務 - 處理直接方法請求

  1. connectIoTHub 函數之前添加以下代碼:

    int directMethodCallback(const char *method_name, const unsigned char *payload, size_t size, unsigned char **response, size_t *response_size, void *userContextCallback)
    {
    Serial.printf("Direct method received %s\r\n", method_name);

    if (strcmp(method_name, "relay_on") == 0)
    {
    digitalWrite(PIN_WIRE_SCL, HIGH);
    }
    else if (strcmp(method_name, "relay_off") == 0)
    {
    digitalWrite(PIN_WIRE_SCL, LOW);
    }
    }

    這段代碼定義了一個回調方法,當物聯網中心庫接收到直接方法請求時可以調用。請求的方法在 method_name 參數中傳遞。這個函數將調用的方法打印到串行端口,然後根據方法名稱打開或關閉繼電器。

    💁 這也可以在單個直接方法請求中實現,將繼電器的期望狀態作為有效負載傳遞,並在方法請求中可從 payload 參數中獲取。

  2. directMethodCallback 函數的末尾添加以下代碼:

    char resultBuff[16];
    sprintf(resultBuff, "{\"Result\":\"\"}");
    *response_size = strlen(resultBuff);
    *response = (unsigned char *)malloc(*response_size);
    memcpy(*response, resultBuff, *response_size);

    return IOTHUB_CLIENT_OK;

    直接方法請求需要一個響應,響應分為兩部分 - 作為文本的響應和返回碼。這段代碼將創建一個結果作為以下 JSON 文檔:

    {
    "Result": ""
    }

    然後將其複製到 response 參數中,並在 response_size 參數中設置此響應的大小。這段代碼然後返回 IOTHUB_CLIENT_OK 以顯示方法已正確處理。

  3. 通過在 connectIoTHub 函數的末尾添加以下代碼來連接回調:

    IoTHubClient_LL_SetDeviceMethodCallback(_device_ll_handle, directMethodCallback, NULL);
  4. loop 函數將調用 IoTHubDeviceClient_LL_DoWork 函數來處理物聯網中心發送的事件。由於 delay,這只會每 10 秒調用一次,這意味著直接方法每 10 秒才處理一次。為了提高效率,可以將 10 秒的延遲實現為多個較短的延遲,每次調用 IoTHubDeviceClient_LL_DoWork。為此,在 loop 函數上方添加以下代碼:

    void work_delay(int delay_time)
    {
    int current = 0;
    do
    {
    IoTHubDeviceClient_LL_DoWork(_device_ll_handle);
    delay(100);
    current += 100;
    } while (current < delay_time);
    }

    這段代碼將重複循環,調用 IoTHubDeviceClient_LL_DoWork 並每次延遲 100 毫秒。它將根據需要多次這樣做,以延遲 delay_time 參數給定的時間量。這意味著設備最多等待 100 毫秒來處理直接方法請求。

  5. loop 函數中,移除對 IoTHubDeviceClient_LL_DoWork 的調用,並將 delay(10000) 調用替換為以下調用這個新函數:

    work_delay(10000);

💁 你可以在 code/wio-terminal 文件夾中找到這段代碼。

😀 你的土壤濕度傳感器程序已連接到你的物聯網中心!