Kết nối và gửi dữ liệu

Phần này sẽ hướng dẫn chi tiết từng bước để bạn lập trình cho mạch Yolo UNO hoặc Yolo Node kết nối và gửi dữ liệu lên Core IoT bằng Arduino IDE:

Bước 1: Chuẩn Bị Môi Trường Arduino IDE #

Cài đặt Arduino IDE: #

Nếu bạn chưa cài đặt Arduino IDE, hãy tải xuống và cài đặt phiên bản mới nhất từ trang web chính thức của Arduino.

Cài đặt Board Yolo UNO hoặc Yolo Node hoặc board ESP32 bất kỳ: #

Mở Arduino IDE, vào File -> Preferences.

Trong ô “Additional Boards Manager URLs“, thêm 2 link sau, mỗi link trên 1 hàng:

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
https://raw.githubusercontent.com/AITT-VN/ohstem_arduino_board/refs/heads/main/package_ohstem_index.json

Nếu bạn đang dùng board Yolo UNO hoặc Yolo Node của OhStem, vào Tools -> Board -> Boards Manager.... Tìm kiếm “ohstem” và cài đặt “OhStem Boards by Ohstem Education“.

Trường hợp bạn dùng board ESP32 khác, vào Tools -> Board -> Boards Manager.... Tìm kiếm “esp32” và cài đặt gói “esp32 by Espressif Systems“.

Cài đặt thư viện cần thiết: #

Trường hợp bạn đã cài đặt board OhStem thì không cần làm bước này vì thư viện Core IoT đã được tích hợp sẵn khi bạn download và dùng các board của OhStem.

Nếu bạn dùng board Esp32 khác, bạn cần download thư viện Core IoT cho Arduino ở đây:

https://github.com/ohstem-public/coreiot-client-sdk

Vào Sketch -> Include Library -> Add .ZIP library.... Chọn đến file thư viện vừa download ở link trên để cái đặt:

Lập Trình thiết bị trên Arduino #

Bạn có thể mở đoạn code example của thư viện CoreIoT – Thingsboard bằng cách vào File > Examples > CoreIoT – Thingsboard > 0004-esp8266_esp32_process_attributes hoặc dùng chướng trình dưới đây.

Chương trình mẫu này kết nối ESP32 với Core IoT và gửi dữ liệu cảm biến nhiệt độ và độ ẩm (giả lập) cũng như xử lý các command thông qua RPC call.

#if defined(ESP8266)
#include <ESP8266WiFi.h>
#define THINGSBOARD_ENABLE_PROGMEM 0
#elif defined(ESP32) || defined(RASPBERRYPI_PICO) || defined(RASPBERRYPI_PICO_W)
#include <WiFi.h>
#endif

#include <Arduino_MQTT_Client.h>
#include <Server_Side_RPC.h>
#include <Attribute_Request.h>
#include <Shared_Attribute_Update.h>
#include <ThingsBoard.h>

// Use virtual random sensor by default

// Uncomment if using DHT20
/*
#include "DHT20.h"
#include "Wire.h"
DHT20 dht20;
*/

// Uncomment if using DHT11
/*
#include <SimpleDHT.h>
#define DHT_PIN D3
SimpleDHT11 dht(DHT_PIN);
*/

// Uncomment if using DHT22
/*
#include <SimpleDHT.h>
#define DHT_PIN D3
SimpleDHT22 dht(DHT_PIN);
*/

#define LED_BUILTIN D13

constexpr char WIFI_SSID[] = "YOUR_WIFI_SSID";
constexpr char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD";

// Your device access token
constexpr char TOKEN[] = "YOUR_ACCESS_TOKEN";

// Server and port we want to establish a connection to
constexpr char COREIOT_SERVER[] = "app.coreiot.io";
constexpr uint16_t COREIOT_PORT = 1883U;

// Maximum packet size to be sent or received by the underlying MQTT client
constexpr uint16_t MAX_MESSAGE_SEND_SIZE = 128U;
constexpr uint16_t MAX_MESSAGE_RECEIVE_SIZE = 128U;

// Maximum number of attributs to request or subscribe
constexpr size_t MAX_ATTRIBUTES = 3U;

constexpr uint64_t REQUEST_TIMEOUT_MICROSECONDS = 5000U * 1000U;

// Serial debug baud rate
constexpr uint32_t SERIAL_DEBUG_BAUD = 115200U;

// Telemetry settings
constexpr int16_t TELEMETRY_SEND_INTERVAL = 5000U;
uint32_t previousTelemetrySend;

constexpr char TEMPERATURE_KEY[] = "temperature";
constexpr char HUMIDITY_KEY[] = "humidity";

// Current led state, on or off
volatile bool ledState = false;
// Current LED brightness: 0 - 255
volatile int ledBrightness = 255; // max by default

// Flag to handle led state and brightness changes
volatile bool ledStateChanged = false;
volatile bool ledBrightessChanged = false;

// Attribute names
constexpr const char LED_STATE_ATTR[] = "led_state";
constexpr const char LED_BRIGHTNESS_ATTR[] = "led_brightness";

// Initalize the Mqtt client instance using WiFi
WiFiClient wifiClient;
Arduino_MQTT_Client mqttClient(wifiClient);

// Initialize apis used
Server_Side_RPC<3U, 5U> rpc; // 3U and 5U are maximum simultaneous server side rpc subscriptions and maximum key-value pairs will be sent
Attribute_Request<2U, MAX_ATTRIBUTES> attr_request;
Shared_Attribute_Update<3U, MAX_ATTRIBUTES> shared_update;

const std::array<IAPI_Implementation*, 3U> apis = {
  &rpc,
  &attr_request,
  &shared_update
};

// List of shared attributes for subscribing to their updates
constexpr std::array<const char *, 1U> SHARED_ATTRIBUTES_LIST = {
  LED_BRIGHTNESS_ATTR
};

// List of client attributes for requesting them (Using to initialize device states)
constexpr std::array<const char *, 1U> CLIENT_ATTRIBUTES_LIST = {
  LED_STATE_ATTR
};

// Initialize ThingsBoard instance with the maximum needed buffer size
ThingsBoard tb(mqttClient, MAX_MESSAGE_RECEIVE_SIZE, MAX_MESSAGE_SEND_SIZE, Default_Max_Stack_Size, apis);

/// @brief Initalizes WiFi connection,
// wait until a connection established
void InitWiFi() {
  Serial.println("Connecting to WiFi AP ...");

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    // Delay 500ms until connected
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected to WiFi AP");
}

/// @brief Reconnects if WiFi disconnected
/// @return Returns true when connected again
const bool reconnect() {
  // Check to ensure we aren't connected yet
  const wl_status_t status = WiFi.status();
  if (status == WL_CONNECTED) {
    return true;
  }

  // If not connected, trying to connect to the given WiFi network
  InitWiFi();
  return true;
}

/// @brief Processes function for RPC call "setLedState"
/// @param data RPC call data from server
void processSetLedState(const JsonVariantConst &data, JsonDocument &response) {
  // Process data
  ledState = data;
  Serial.print("Received set led state RPC. New state: ");
  Serial.println(ledState);

  StaticJsonDocument<1> response_doc;
  // Returning current state as response
  response_doc["newState"] = (int)ledState;
  response.set(response_doc);

  ledStateChanged = true;
}

// Optional, keep subscribed shared attributes empty instead,
// and the callback will be called for every shared attribute changed on the device,
// instead of only the one that were entered instead
const std::array<RPC_Callback, 1U> rpcCallbacks = {
  RPC_Callback{ "setLedState", processSetLedState }
};

/// @brief Shared attribute update callback
/// @param data New value of shared attributes which is changed
void processSharedAttributes(const JsonObjectConst &data) {
  for (auto it = data.begin(); it != data.end(); ++it) {
    if (strcmp(it->key().c_str(), LED_BRIGHTNESS_ATTR) == 0) {
      const uint16_t newBrightess = it->value().as<uint16_t>();
      if (newBrightess >= 0 && newBrightess <= 255) {
        ledBrightness = newBrightess;
        Serial.print("Led brightness is set to: ");
        Serial.println(ledBrightness);
      }
    }
  }
  ledBrightessChanged = true;
}

void processClientAttributes(const JsonObjectConst &data) {
  for (auto it = data.begin(); it != data.end(); ++it) {
    if (strcmp(it->key().c_str(), LED_STATE_ATTR) == 0) {
      ledState = it->value().as<bool>();
      digitalWrite(LED_BUILTIN, ledState);
    }
  }
}

// Attribute request did not receive a response in the expected amount of microseconds 
void requestTimedOut() {
  Serial.printf("Attribute request not receive response in (%llu) microseconds. Ensure client is connected to the MQTT broker and that the keys actually exist on the target device\n", REQUEST_TIMEOUT_MICROSECONDS);
}

const Shared_Attribute_Callback<MAX_ATTRIBUTES> attributes_callback(&processSharedAttributes, SHARED_ATTRIBUTES_LIST.cbegin(), SHARED_ATTRIBUTES_LIST.cend());
const Attribute_Request_Callback<MAX_ATTRIBUTES> attribute_shared_request_callback(&processSharedAttributes, REQUEST_TIMEOUT_MICROSECONDS, &requestTimedOut, SHARED_ATTRIBUTES_LIST);
const Attribute_Request_Callback<MAX_ATTRIBUTES> attribute_client_request_callback(&processClientAttributes, REQUEST_TIMEOUT_MICROSECONDS, &requestTimedOut, CLIENT_ATTRIBUTES_LIST);


void setup() {

  // Uncomment if using DHT20
  //Wire.begin(SDA, SCL);
  //dht20.begin();

  // Initialize serial output for debugging
  Serial.begin(SERIAL_DEBUG_BAUD);
  
  // Turn off LED at boot
  pinMode(LED_BUILTIN, OUTPUT);
  analogWrite(LED_BUILTIN, 0);

  delay(1000);
  InitWiFi();
}

void loop() {
  delay(10);

  if (!reconnect()) {
    return;
  }

  if (!tb.connected()) {
    // Connect to the server
    Serial.print("Connecting to: ");
    Serial.print(COREIOT_SERVER);
    Serial.print(" with token ");
    Serial.println(TOKEN);
    if (!tb.connect(COREIOT_SERVER, TOKEN, COREIOT_PORT)) {
      Serial.println("Failed to connect");
      return;
    } else {
      Serial.println("Connected to server");
    }
    // Sending a MAC and IP address as an attribute
    tb.sendAttributeData("mac_address", WiFi.macAddress().c_str());
    tb.sendAttributeData("ip_address", WiFi.localIP().toString().c_str());
    tb.sendAttributeData("ssid", WiFi.SSID().c_str());
    tb.sendAttributeData("bssid", WiFi.BSSIDstr().c_str());    
    tb.sendAttributeData("channel", WiFi.channel());

    Serial.println("Subscribing for RPC...");
    if (!rpc.RPC_Subscribe(rpcCallbacks.cbegin(), rpcCallbacks.cend())) {
      Serial.println("Failed to subscribe for RPC");
      return;
    }

    if (!shared_update.Shared_Attributes_Subscribe(attributes_callback)) {
      Serial.println("Failed to subscribe for shared attribute updates");
      return;
    }

    Serial.println("Subscribe shared attributes done");

    // Request current value of shared attributes
    if (!attr_request.Shared_Attributes_Request(attribute_shared_request_callback)) {
      Serial.println("Failed to request for shared attributes (led brightness)");
      return;
    }

    // Request current states of client attributes
    if (!attr_request.Client_Attributes_Request(attribute_client_request_callback)) {
      Serial.println("Failed to request for client attributes (led state)");
      return;
    }
  }

  // Sending telemetry by time interval
  if (millis() - previousTelemetrySend > TELEMETRY_SEND_INTERVAL) {

    // Use virtual sensor with random value
    float temperature = random(20, 40);
    float humidity = random(50, 100);
    
    // Uncomment if using DHT20
    /*    
    dht20.read();    
    float temperature = dht20.getTemperature();
    float humidity = dht20.getHumidity();
    */

    // Uncomment if using DHT11/22
    /*    
    float temperature = 0;
    float humidity = 0;
    dht.read2(&temperature, &humidity, NULL);
    */

    Serial.println("Sending telemetry. Temperature: " + String(temperature, 1) + " humidity: " + String(humidity, 1));

    tb.sendTelemetryData(TEMPERATURE_KEY, temperature);
    tb.sendTelemetryData(HUMIDITY_KEY, humidity);
    tb.sendAttributeData("rssi", WiFi.RSSI()); // also update wifi signal strength
    previousTelemetrySend = millis();
  }

  if (ledStateChanged || ledBrightessChanged) {
    ledStateChanged = false;
    ledBrightessChanged = false;

    if (ledState) {
      analogWrite(LED_BUILTIN, ledBrightness);
    } else {
      analogWrite(LED_BUILTIN, 0);
    }
    Serial.print("LED state is set to: ");
    Serial.println(ledState);

    tb.sendAttributeData(LED_STATE_ATTR, ledState);
  }

  tb.loop();
}

Chúng ta cùng tìm hiểu chi tiết của chương trình Arduino trên.

// Use virtual random sensor by default

// Uncomment if using DHT20
/*
#include "DHT20.h"
#include "Wire.h"
DHT20 dht20;
*/

// Uncomment if using DHT11
/*
#include <SimpleDHT.h>
#define DHT_PIN D3
SimpleDHT11 dht(DHT_PIN);
*/

// Uncomment if using DHT22
/*
#include <SimpleDHT.h>
#define DHT_PIN D3
SimpleDHT22 dht(DHT_PIN);
*/

#define LED_BUILTIN D13

Đoạn code này viết sẵn cho bạn sử dụng với các loại cảm biến khác nhau là DHT20, DHT11, DHT22 và khai báo chân IO nối với đèn LED sẽ được điều khiển bật tắt từ Core IoT. Mặc định chương trình sẽ giả lập các thông tin nhiệt độ và độ ẩm bằng các giá ngẫu nhiên. Nếu bạn có sử dụng 1 trong các cảm biến DHT thì bạn hãy mở comment các dòng code tương ứng.

Bạn cũng cần sửa các thông tin bao gồm thông tin mạng WiFi và quan trọng là access token của thiết bị đã tạo ở phần hướng dẫn trước cho đúng.

constexpr char WIFI_SSID[] = "YOUR_WIFI_SSID";
constexpr char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD";

// Your device access token
constexpr char TOKEN[] = "YOUR_ACCESS_TOKEN";
constexpr char TEMPERATURE_KEY[] = "temperature";
constexpr char HUMIDITY_KEY[] = "humidity";

// Current led state, on or off
volatile bool ledState = false;
// Current LED brightness: 0 - 255
volatile int ledBrightness = 255; // max by default

// Flag to handle led state and brightness changes
volatile bool ledStateChanged = false;
volatile bool ledBrightessChanged = false;

// Attribute names
constexpr const char LED_STATE_ATTR[] = "led_state";
constexpr const char LED_BRIGHTNESS_ATTR[] = "led_brightness";

Đoạn code trên khai báo các Telemetry key dùng để gửi thông tin lên Core IoT là temperature (cho nhiệt độ) và humidity (cho độ ẩm). Đồng thời cũng khai báo tên attribute để lưu trạng thái bật tắt của đèn LED (led_state) và độ sáng của đèn (led_brightness).

/// @brief Processes function for RPC call "setLedState"
/// @param data RPC call data from server
void processSetLedState(const JsonVariantConst &data, JsonDocument &response) {
  // Process data
  ledState = data;
  Serial.print("Received set led state RPC. New state: ");
  Serial.println(ledState);

  StaticJsonDocument<1> response_doc;
  // Returning current state as response
  response_doc["newState"] = (int)ledState;
  response.set(response_doc);

  ledStateChanged = true;
}

// Optional, keep subscribed shared attributes empty instead,
// and the callback will be called for every shared attribute changed on the device,
// instead of only the one that were entered instead
const std::array<RPC_Callback, 1U> rpcCallbacks = {
  RPC_Callback{ "setLedState", processSetLedState }
};

Đoạn code trên khai báo hàm xử lý khi có lệnh RPC có tên là setLedState từ Dashboard Core IoT gửi xuống. Trong hàm xử lý, ta đọc giá trị được gửi xuống và lưu vào biến ledState để xử lý trong vòng lặp chính (bật hoặc tắt đèn LED).

/// @brief Shared attribute update callback
/// @param data New value of shared attributes which is changed
void processSharedAttributes(const JsonObjectConst &data) {
  for (auto it = data.begin(); it != data.end(); ++it) {
    if (strcmp(it->key().c_str(), LED_BRIGHTNESS_ATTR) == 0) {
      const uint16_t newBrightess = it->value().as<uint16_t>();
      if (newBrightess >= 0 && newBrightess <= 255) {
        ledBrightness = newBrightess;
        Serial.print("Led brightness is set to: ");
        Serial.println(ledBrightness);
      }
    }
  }
  ledBrightessChanged = true;
}

void processClientAttributes(const JsonObjectConst &data) {
  for (auto it = data.begin(); it != data.end(); ++it) {
    if (strcmp(it->key().c_str(), LED_STATE_ATTR) == 0) {
      ledState = it->value().as<bool>();
      digitalWrite(LED_BUILTIN, ledState);
    }
  }
}

Đoạn code trên khai báo hàm xử lý khi có cập nhật attribute liên quan đến trạng thái đèn LED (khi thiết bị mới boot, sẽ gửi yêu cầu trạng thái hiện tại của đèn LED từ server để bật/tắt) và độ sáng của đèn.

if (!tb.connected()) {
    // Connect to the server
    Serial.print("Connecting to: ");
    Serial.print(COREIOT_SERVER);
    Serial.print(" with token ");
    Serial.println(TOKEN);
    if (!tb.connect(COREIOT_SERVER, TOKEN, COREIOT_PORT)) {
      Serial.println("Failed to connect");
      return;
    } else {
      Serial.println("Connected to server");
    }
    // Sending a MAC and IP address as an attribute
    tb.sendAttributeData("mac_address", WiFi.macAddress().c_str());
    tb.sendAttributeData("ip_address", WiFi.localIP().toString().c_str());
    tb.sendAttributeData("ssid", WiFi.SSID().c_str());
    tb.sendAttributeData("bssid", WiFi.BSSIDstr().c_str());    
    tb.sendAttributeData("channel", WiFi.channel());

    Serial.println("Subscribing for RPC...");
    if (!rpc.RPC_Subscribe(rpcCallbacks.cbegin(), rpcCallbacks.cend())) {
      Serial.println("Failed to subscribe for RPC");
      return;
    }

    if (!shared_update.Shared_Attributes_Subscribe(attributes_callback)) {
      Serial.println("Failed to subscribe for shared attribute updates");
      return;
    }

    Serial.println("Subscribe shared attributes done");

    // Request current value of shared attributes
    if (!attr_request.Shared_Attributes_Request(attribute_shared_request_callback)) {
      Serial.println("Failed to request for shared attributes (led brightness)");
      return;
    }

    // Request current states of client attributes
    if (!attr_request.Client_Attributes_Request(attribute_client_request_callback)) {
      Serial.println("Failed to request for client attributes (led state)");
      return;
    }
  }

Đoạn code trên kiểm tra nếu thiết bị chưa được kết nối với server Core IoT thì sẽ tiến hành kết. Sau khi kết nối thành công sẽ gửi 1 loạt các thông tin của thiết bị dưới dạng các attribute. Đồng thời cũng đăng ký các hàm xử lý khi có cập nhật các giá trị attribute từ server.

  // Sending telemetry by time interval
  if (millis() - previousTelemetrySend > TELEMETRY_SEND_INTERVAL) {

    // Use virtual sensor with random value
    float temperature = random(20, 40);
    float humidity = random(50, 100);
    
    // Uncomment if using DHT20
    /*    
    dht20.read();    
    float temperature = dht20.getTemperature();
    float humidity = dht20.getHumidity();
    */

    // Uncomment if using DHT11/22
    /*    
    float temperature = 0;
    float humidity = 0;
    dht.read2(&temperature, &humidity, NULL);
    */

    Serial.println("Sending telemetry. Temperature: " + String(temperature, 1) + " humidity: " + String(humidity, 1));

    tb.sendTelemetryData(TEMPERATURE_KEY, temperature);
    tb.sendTelemetryData(HUMIDITY_KEY, humidity);
    tb.sendAttributeData("rssi", WiFi.RSSI()); // also update wifi signal strength
    previousTelemetrySend = millis();
  }

Phần code gửi dữ liệu cảm biến và thông tin thiết bị (mặc định giả lập bằng các giá trị ngẫu nhiên). Bạn có thể uncomment code cho cảm biến mà bạn có.