12 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 data 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
-
Open the
soil-moisture-sensorproject in VS Code. -
Open the
platformio.inifile. Remove theknolleary/PubSubClientlibrary dependency. This library was used to connect to the public MQTT broker, but it is not required for connecting to IoT Hub. -
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.2The
Seeed Arduino RTClibrary provides functionality to interact with the real-time clock in the Wio Terminal, which is used to keep track of time. The other libraries enable your IoT device to connect to IoT Hub. -
Add the following line to the bottom of the
platformio.inifile:build_flags = -DDONT_USE_UPLOADTOBLOBThis sets a compiler flag required for compiling the Arduino IoT Hub code.
-
Open the
config.hheader 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 that you copied earlier. -
The connection to IoT Hub uses a time-based token, which means the IoT device needs to know the current time. Unlike operating systems such as Windows, macOS, or Linux, microcontrollers don't automatically synchronize the current time over the Internet. Therefore, you need to add code to retrieve the current time from an NTP server. Once the time is retrieved, it can be stored in the Wio Terminal's real-time clock, allowing the device to request the correct time later, as long as it doesn't lose power. Add a new file called
ntp.hwith 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 beyond the scope of this lesson. It defines a function called
initTimethat retrieves the current time from an NTP server and sets the clock on the Wio Terminal. -
Open the
main.cppfile and remove all the MQTT-related code, including thePubSubClient.hheader file, the declaration of thePubSubClientvariable, thereconnectMQTTClientandcreateMQTTClientmethods, and any calls to these variables and methods. This file should only contain code for connecting to WiFi, retrieving soil moisture data, and creating a JSON document with the data. -
Add the following
#includedirectives to the top of themain.cppfile 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" -
Add the following call to the end of the
setupfunction to set the current time:initTime(); -
Add the following variable declaration to the top of the file, just below the include directives:
IOTHUB_DEVICE_CLIENT_LL_HANDLE _device_ll_handle;This declares an
IOTHUB_DEVICE_CLIENT_LL_HANDLE, which is a handle for the connection to IoT Hub. -
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 triggered when the connection to IoT Hub changes status, such as connecting or disconnecting. The status will be printed to the serial port.
-
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, then creates a connection using the connection string from the
config.hheader file. The connection is based on MQTT. If the connection fails, the error will be printed to the serial port—check the connection string if you see this in the output. Finally, the connection status callback is set up. -
Call this function in the
setupfunction, below the call toinitTime:connectIoTHub(); -
Similar to the MQTT client, this code runs on a single thread and requires time to process messages sent by and to the hub. Add the following to the top of the
loopfunction to handle this:IoTHubDeviceClient_LL_DoWork(_device_ll_handle); -
Build and upload the code. You will see the connection status 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 iothubIn the output, you will see the NTP time being fetched, followed by the device client connecting. It may take a few seconds to connect, so you might see soil moisture data in the output while the device is still connecting.
💁 You can convert the UNIX time from the NTP server into a more readable format using a website like unixtimestamp.com.
Send telemetry
Now that your device is connected, you can send telemetry data to IoT Hub instead of the MQTT broker.
Task - Send telemetry
-
Add the following function above the
setupfunction: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, and then cleans up the message object.
-
Call this function in the
loopfunction, just after the line where telemetry data is sent to the serial port:sendTelemetry(telemetry.c_str());
Handle commands
Your device needs to handle commands from the server code to control the relay. These commands are sent as direct method requests.
Task - Handle a direct method request
-
Add the following code before the
connectIoTHubfunction: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 function that the IoT Hub library can call when it receives a direct method request. The requested method is passed in the
method_nameparameter. This function prints the method name to the serial port and turns the relay on or off based on the method name.💁 This could also be implemented using a single direct method request, where the desired relay state is passed in a payload available from the
payloadparameter. -
Add the following code to the end of the
directMethodCallbackfunction: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 require a response, which consists of two parts: a response message and a return code. This code creates a result in the form of the following JSON document:
{ "Result": "" }The result is copied into the
responseparameter, and the size of the response is set in theresponse_sizeparameter. The function then returnsIOTHUB_CLIENT_OKto indicate that the method was handled successfully. -
Wire up the callback by adding the following line to the end of the
connectIoTHubfunction:IoTHubClient_LL_SetDeviceMethodCallback(_device_ll_handle, directMethodCallback, NULL); -
The
loopfunction calls theIoTHubDeviceClient_LL_DoWorkfunction to process events sent by IoT Hub. Currently, this is only called every 10 seconds due to thedelay, meaning direct methods are processed only every 10 seconds. To make this more efficient, the 10-second delay can be implemented as multiple shorter delays, callingIoTHubDeviceClient_LL_DoWorkeach time. Add the following code above theloopfunction: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 loops repeatedly, calling
IoTHubDeviceClient_LL_DoWorkand delaying for 100ms each time. It repeats this process as many times as needed to achieve the delay specified in thedelay_timeparameter. This ensures the device waits no longer than 100ms to process direct method requests. -
In the
loopfunction, remove the call toIoTHubDeviceClient_LL_DoWorkand replace thedelay(10000)call with the following line to use the new function:work_delay(10000);
💁 You can find this code in the code/wio-terminal folder.
😀 Your soil moisture sensor program is now connected to your IoT Hub!
Disclaimer:
This document has been translated using the AI translation service Co-op Translator. While we aim for accuracy, please note that automated translations may include errors or inaccuracies. The original document in its native language should be regarded as the authoritative source. For critical information, professional human translation is advised. We are not responsible for any misunderstandings or misinterpretations resulting from the use of this translation.