You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
IoT-For-Beginners/2-farm/lessons/4-migrate-your-plant-to-the.../wio-terminal-connect-hub.md

11 KiB

Connect your IoT device to the cloud - Wio Terminal

In this part of the lesson, you will connect your Wio Terminal to your IoT Hub, to send telemetry and receive commands.

Connect your device to IoT Hub

The next step is to connect your device to IoT Hub.

Task - connect to IoT Hub

  1. Open the soil-moisture-sensor project in VS Code

  2. Open the platformio.ini file. Remove the knolleary/PubSubClient library dependency. This was used to connect to the public MQTT broker, and is not needed to connect to IoT Hub.

  3. Add the following library dependencies:

    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
    

    The Seeed Arduino RTC library provides code to interact with a real-time clock in the Wio Terminal, used to track the time. The remaining libraries allow your IoT device to connect to IoT Hub.

  4. Add the following to the bottom of the platformio.ini file:

    build_flags =
        -DDONT_USE_UPLOADTOBLOB
    

    This sets a compiler flag that is needed when compiling the Arduino IoT Hub code.

  5. Open the config.h header file. Remove all the MQTT settings and add the following constant for the device connection string:

    // IoT Hub settings
    const char *CONNECTION_STRING = "<connection string>";
    

    Replace <connection string> with the connection string for your device you copied earlier.

  6. The connection to IoT Hub uses a time-based token. This means the IoT device needs to know the current time. Unlike operating systems like Windows, macOS or Linux, microcontrollers don't automatically synchronize the current time over the Internet. This means you will need to add code to get the current time from an NTP server. Once the time has been retrieved, it can be stored in a real-time clock in the Wio Terminal, allowing the correct time to be requested at a later date, assuming the device doesn't lose power. Add a new file called ntp.h with the following code:

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

    The details of this code are outside the scope of this lesson. It defines a function called initTime that gets the current time from an NTP server and uses it to set the clock on the Wio Terminal.

  7. Open the main.cpp file and remove all the MQTT code, including the PubSubClient.h header file, the declaration of the PubSubClient variable, the reconnectMQTTClient and createMQTTClient methods, and any calls to these variables and methods. This file should only contain code to connect to WiFi, get the soil moisture and create a JSON document with it in.

  8. Add the following #include directives to the top of the main.cpp file to include header files for the IoT Hub libraries, and for setting the time:

    #include <AzureIoTHub.h>
    #include <AzureIoTProtocol_MQTT.h>
    #include <iothubtransportmqtt.h>
    #include "ntp.h"
    
  9. Add the following call to the end of the setup function to set the current time:

    initTime();
    
  10. Add the following variable declaration to the top of the file, just below the include directived:

    IOTHUB_DEVICE_CLIENT_LL_HANDLE _device_ll_handle;
    

    This declares an IOTHUB_DEVICE_CLIENT_LL_HANDLE, a handle to a connection to the IoT Hub.

  11. Below this, add the following code:

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

    This declares a callback function that will be called when the connection to the IoT Hub changes status, such as connecting or disconnecting. The status is sent to the serial port.

  12. Below this, add a function to connect to IoT Hub:

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

    This code initializes the IoT Hub library code, then creates a connection using the connection string in the config.h header file. This connection is based on MQTT. If the connection fails, this is sent to the serial port - if you see this in the output, check the connection string. Finally the connection status callback is set up.

  13. Call this function in the setup function below the call to initTime:

    connectIoTHub();
    
  14. Just like with the MQTT client, this code runs on a single thread so needs time to process messages being sent by the hub, and sent to the hub. Add the following to the top of the loop function to do this:

    IoTHubDeviceClient_LL_DoWork(_device_ll_handle);
    
  15. Build and upload this code. You will see the connection in the serial monitor:

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

    In the output you can see the NTP time being fetched, followed by the device client connecting. It can take a few seconds to connect, so you may see the soil moisture in the output whilst the device is connecting.

    💁 You can convert the UNIX time for the NTP to a more readable version using a web site like unixtimestamp.com

Send telemetry

Now that your device is connected, you can send telemetry to the IoT Hub instead of the MQTT broker.

Task - send telemetry

  1. Add the following function above the setup function:

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

    This code creates an IoT Hub message from a string passed as a parameter, sends it to the hub, then cleans up the message object.

  2. Call this code in the loop function, just after the line where the telemetry is sent to the serial port:

    sendTelemetry(telemetry.c_str());
    

Handle commands

Your device needs to handle a command from the server code to control the relay. This is sent as a direct method request.

Task - handle a direct method request

  1. Add the following code before the connectIoTHub function:

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

    This code defines a callback method that the IoT Hub library can call when it receives a direct method request. The method that is requested is sent in the method_name parameter. This function prints the method called to the serial port, then turns the relay on or off depending on the method name.

    💁 This could also be implemented in a single direct method request, passing the desired state of the relay in a payload that can be passed with the method request and available from the payload parameter.

  2. Add the following code to the end of the directMethodCallback function:

    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;
    

    Direct method requests need a response, and the response is in two parts - a response as text, and a return code. This code will create a result as the following JSON document:

    {
        "Result": ""
    }
    

    This is then copied into the response parameter, and the size of this response is set in the response_size parameter. This code then returns IOTHUB_CLIENT_OK to show the method was handled correctly.

  3. Wire up the callback by adding the following to the end of the connectIoTHub function:

    IoTHubClient_LL_SetDeviceMethodCallback(_device_ll_handle, directMethodCallback, NULL);
    
  4. The loop function will call the IoTHubDeviceClient_LL_DoWork function to process events send by IoT Hub. This is only called every 10 seconds due to the delay, meaning direct methods are only processed every 10 seconds. To make this more efficient, the 10 second delay can be implemented as many shorter delays, calling IoTHubDeviceClient_LL_DoWork each time. To do this, add the following code above the loop function:

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

    This code will loop repeatedly, calling IoTHubDeviceClient_LL_DoWork and delaying for 100ms each time. It will do this as many times as needed to delay for the amount of time given in the delay_time parameter. This means the device is waiting at most 100ms to process direct method requests.

  5. In the loop function, remove the call to IoTHubDeviceClient_LL_DoWork, and replace the delay(10000) call with the following to call this new function:

    work_delay(10000);
    

💁 You can find this code in the code/wio-terminal folder.

😀 Your soil moisture sensor program is connected to your IoT Hub!