Hướng dẫn LoRa ESP32 Arduino – Giám sát cảm biến với Web Server

Trong dự án LoRa ESP32 Arduino này, chúng ta sẽ xây dựng một hệ thống để giám sát dữ liệu thu được từ cảm biến và gửi thông tin này qua LoRa đến máy thu ESP32. Bộ thu này sẽ hiển thị kết quả mới nhất từ cảm biến lên Web Server, để chúng ta theo dõi được từ xa qua điện thoại hoặc các thiết bị điện tử.

Cụ thể, IoTZone sẽ hướng dẫn bạn cách:

  • Gửi kết quả đọc được từ cảm biến qua đài LoRa giữa 2 mạch ESP32
  • Thêm các tính năng WiFi và LoRa vào dự án của bạn
  • Sử dụng mạch TTGO LoRa32 SX1276 OLED hoặc các mạch tương tự khác cho các dự án IoT của bạn

Giới thiệu dự án LoRa ESP32 Arduino

Dưới đây là hình ảnh minh họa về dự án mà mình sẽ hướng dẫn bạn:

Giới thiệu dự án LoRa ESP32 Arduino

Cụ thể như sau:

  • Thiết bị gửi LoRa gửi giá trị của cảm biến BME280 qua đài LoRa (tần suất 10 giây 1 lần)
  • Thiết bị nhận LoRa nhận thông tin và hiển thị trên Web Server
  • Bạn có thể giám sát thông tin nhận được từ cảm biến bằng cách vào Web Server để xem
  • Thiết bị gửi và nhận LoRa có thể cách xa nhau vài trăm mét. Do đó, bạn có thể sử dụng dự án LoRa ESP32 Arduino này để theo dõi số liệu từ cánh đồng trồng trọt của mình, nếu nhà bạn chỉ cách đó vài trăm mét.
  • Thiết bị nhận LoRa hoạt động trên một máy chủ Web không đồng bộ, và các file của Website được lưu trên ESP32 filesystem (SPIFFS)
  • Thiết bị nhận LoRa cũng hiển thị ngày và giờ nhận được dữ liệu gần nhất. Để có dữ liệu thời gian này, chúng ta sử dụng đến giao thức NTP trên ESP32

Chuẩn bị

  • Mạch TTGO LoRa32 SX1276 OLED: Đây là mạch ESP32 tích hợp OLED và LoRa, bạn có thể sử dụng các mạch ESP32 tương tự tại đây
  • Cảm biến nhiệt độ, độ ẩm và áp suất BME280. Tuy nhiên bạn có thể sử dụng các cảm biến khác tùy thích
Chuẩn bị thiết bị cho dự án LoRa ESP32 Arduino

Cài đặt thư viện trên Arduino IDE

Trước khi cài thư viện, bạn cần phải cài đặt gói ESP32 trong phần mềm Arduino IDE của mình, để có thể làm việc với mạch này. Nếu chưa cài, bạn xem hướng dẫn bên dưới, nếu đã cài, bạn bỏ qua bước này và trực tiếp kéo xuống phần cài thư viện.

Cách lập trình ESP32 bằng Arduino IDE (Windows, Linux, Mac OS X)

Thư viện OLED, BME280 và LoRa

Bạn có thể cài đặt 3 thư viện trên thông qua trình quản lý thư viện có sẵn trong Arduino IDE, bằng cách truy cập Sketch > Include Library> Manage Libraries và tìm kiếm tên 3 thư viện sau:

Thư viện Web không đồng bộ (Asynchronous Web Server)

Bạn cần cài đặt 2 thư viện sau:

2 thư viện trên không có sẵn trong trình quản lý thư viện của Arduino IDE, do đó, bạn cần tải chúng trực tiếp qua link mình đã gắn bên trên, sau đó giải nén và di chuyển chúng vào thư mục thư viện của Arduino.

Sau đó, bạn mở Sketch > Include Library > Add .ZIP library… và chọn thư mục bạn vừa tải về.

Thư viện NTPClient

Mỗi khi thiết bị nhận LoRa nhận một tin nhắn mới, chúng sẽ yêu cầu ngày và giờ từ NTP Server để biết thời gian nhận tin là khi nào.

Để thực hiện được điều đó, trong dự án LoRa ESP32 Arduino lần này, chúng ta sẽ sử dụng thư viện NTPClient. Tương tự, thư viện này không có sẵn trong trình quản lý, nên bạn cần tải file về máy, giải nén và di chuyển vào thư mục thư viện:

Làm việc với thiết bị gửi LoRa

Thiết bị gửi LoRa được kết nối với cảm biến BME280. Chúng có vai trò gửi dữ liệu nhiệt độ, độ ẩm và áp suất sau mỗi 10 giây, bạn có thể thay đổi tần suất này tùy thích thông qua đoạn code.

Kết nối thiết bị

Làm việc với thiết bị gửi LoRa ESP32 Arduino

Code

//Libraries for LoRa
#include <SPI.h>
#include <LoRa.h>

//Libraries for OLED Display
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

//Libraries for BME280
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

//define the pins used by the LoRa transceiver module
#define SCK 5
#define MISO 19
#define MOSI 27
#define SS 18
#define RST 14
#define DIO0 26

//433E6 for Asia
//866E6 for Europe
//915E6 for North America
#define BAND 866E6

//OLED pins
#define OLED_SDA 4
#define OLED_SCL 15 
#define OLED_RST 16
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

//BME280 definition
#define SDA 21
#define SCL 13

TwoWire I2Cone = TwoWire(1);
Adafruit_BME280 bme;

//packet counter
int readingID = 0;

int counter = 0;
String LoRaMessage = "";

float temperature = 0;
float humidity = 0;
float pressure = 0;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST);

//Initialize OLED display
void startOLED(){
  //reset OLED display via software
  pinMode(OLED_RST, OUTPUT);
  digitalWrite(OLED_RST, LOW);
  delay(20);
  digitalWrite(OLED_RST, HIGH);

  //initialize OLED
  Wire.begin(OLED_SDA, OLED_SCL);
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("LORA SENDER");
}

//Initialize LoRa module
void startLoRA(){
  //SPI LoRa pins
  SPI.begin(SCK, MISO, MOSI, SS);
  //setup LoRa transceiver module
  LoRa.setPins(SS, RST, DIO0);

  while (!LoRa.begin(BAND) && counter < 10) {
    Serial.print(".");
    counter++;
    delay(500);
  }
  if (counter == 10) {
    // Increment readingID on every new reading
    readingID++;
    Serial.println("Starting LoRa failed!"); 
  }
  Serial.println("LoRa Initialization OK!");
  display.setCursor(0,10);
  display.clearDisplay();
  display.print("LoRa Initializing OK!");
  display.display();
  delay(2000);
}

void startBME(){
  I2Cone.begin(SDA, SCL, 100000); 
  bool status1 = bme.begin(0x76, &I2Cone);  
  if (!status1) {
    Serial.println("Could not find a valid BME280_1 sensor, check wiring!");
    while (1);
  }
}

void getReadings(){
  temperature = bme.readTemperature();
  humidity = bme.readHumidity();
  pressure = bme.readPressure() / 100.0F;
}

void sendReadings() {
  LoRaMessage = String(readingID) + "/" + String(temperature) + "&" + String(humidity) + "#" + String(pressure);
  //Send LoRa packet to receiver
  LoRa.beginPacket();
  LoRa.print(LoRaMessage);
  LoRa.endPacket();
  
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(1);
  display.print("LoRa packet sent!");
  display.setCursor(0,20);
  display.print("Temperature:");
  display.setCursor(72,20);
  display.print(temperature);
  display.setCursor(0,30);
  display.print("Humidity:");
  display.setCursor(54,30);
  display.print(humidity);
  display.setCursor(0,40);
  display.print("Pressure:");
  display.setCursor(54,40);
  display.print(pressure);
  display.setCursor(0,50);
  display.print("Reading ID:");
  display.setCursor(66,50);
  display.print(readingID);
  display.display();
  Serial.print("Sending packet: ");
  Serial.println(readingID);
  readingID++;
}

void setup() {
  //initialize Serial Monitor
  Serial.begin(115200);
  startOLED();
  startBME();
  startLoRA();
}
void loop() {
  getReadings();
  sendReadings();
  delay(10000);
}

Sau khi nạp code trên vào mạch, bạn sẽ nhận được các thông tin mới nhất từ cảm biến lên màn hình OLED:

Demo thiết bị gửi LoRa ESP32 Arduino

Thiết bị thu LoRa

Thiết bị thu LoRa sẽ nhận các thông tin từ thiết bị gửi LoRa, và hiển thị chúng trên Web Server. Ngoài giá trị từ cảm biến, chúng ta cũng sẽ hiển thị thời gian nhận các chỉ số đó và RSSI (received signal strength indicator – thông số về cường độ tín hiệu đã nhận).

Dưới đây là hình minh họa về Web Server mà chúng ta xây dựng:

Web Server trong dự án LoRa ESP32 Arduino
Web Server trong dự án LoRa ESP32 Arduino

Như bạn thấy, chúng ta sẽ tạo hình nền và các icon trang trí, phân tầng giữa heading và text thường để trang Web trông đẹp mắt và dễ theo dõi hơn.

Có nhiều cách khác nhau để hiển thị hình ảnh trên máy chủ Web. Trong dự án LoRa ESP32 Arduino này, IoTZone sẽ lưu trữ hệ thống trên hệ thống tệp của ESP32 (SPIFFS). Chúng ta cũng sẽ lưu trữ file HTML trên SPIFFS.

Sắp xếp file của bạn

Để xây dựng Web Server, bạn cần 3 file:

  • Sketch trong Arduino
  • File HTML
  • File hình ảnh

File HTML và hình ảnh phải được lưu trong thư mục có tên data bên trong thư mục Arduino sketch, như hình minh họa:

Cách lưu các file trong dự án LoRa ESP32 Arduino
Cách lưu các file trong dự án LoRa ESP32 Arduino

Tạo file HTML

Bạn cần tạo 1 file index.html có nội dung sau:

<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <title>ESP32 (LoRa + Server)</title>
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <style>
    body {
      margin: 0;
      font-family: Arial, Helvetica, sans-serif;
      text-align: center;
    }
    header {
      margin: 0;
      padding-top: 5vh;
      padding-bottom: 5vh;
      overflow: hidden;
      background-image: url(winter);
      background-size: cover;
      color: white;
    }
    h2 {
      font-size: 2.0rem;
    }
    p { font-size: 1.2rem; }
    .units { font-size: 1.2rem; }
    .readings { font-size: 2.0rem; }
  </style>
</head>
<body>
  <header>
    <h2>ESP32 (LoRa + Server)</h2>
    <p><strong>Last received packet:<br/><span id="timestamp">%TIMESTAMP%</span></strong></p>
    <p>LoRa RSSI: <span id="rssi">%RSSI%</span></p>
  </header>
<main>
  <p>
    <i class="fas fa-thermometer-half" style="color:#059e8a;"></i> Temperature: <span id="temperature" class="readings">%TEMPERATURE%</span>
    <sup>&deg;C</sup>
  </p>
  <p>
    <i class="fas fa-tint" style="color:#00add6;"></i> Humidity: <span id="humidity" class="readings">%HUMIDITY%</span>
    <sup>&#37;</sup>
  </p>
  <p>
    <i class="fas fa-angle-double-down" style="color:#e8c14d;"></i> Pressure: <span id="pressure" class="readings">%PRESSURE%</span>
    <sup>hpa</sup>
  </p>
</main>
<script>
setInterval(updateValues, 10000, "temperature");
setInterval(updateValues, 10000, "humidity");
setInterval(updateValues, 10000, "pressure");
setInterval(updateValues, 10000, "rssi");
setInterval(updateValues, 10000, "timestamp");

function updateValues(value) {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById(value).innerHTML = this.responseText;
    }
  };
  xhttp.open("GET", "/" + value, true);
  xhttp.send();
}
</script>
</body>
</html>

Mình cũng đưa các style CSS vào trong file HTML trên, cũng như có thêm các đoạn JavaScropt để hệ thống web tự cập nhật các giá trị từ cảm biến.

Một phần quan trọng bạn cần lưu ý là placeholders (phần giữ chỗ), đây là những phần được đặt giữa các dấu %, ví dụ như %TIMESTAMP%, %TEMPERATURE%, %HUMIDITY%, %PRESSURE% và %RSSI%. Các phần giữa chỗ này sẽ được thay thế thực tế bằng code trong Arduino.

Các style được thêm vào giữa các thẻ <style> và <style>:

<style>
  body {
    margin: 0;
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
  }
  header {
    margin: 0;
    padding-top: 10vh;
    padding-bottom: 5vh;
    overflow: hidden;
    width: 100%;
    background-image: url(winter.jpg);
    background-size: cover;
    color: white;
  }
  h2 {
    font-size: 2.0rem;
  }
  p { font-size: 1.2rem; }
  .units { font-size: 1.2rem; }
  .readings { font-size: 2.0rem; }
</style>

Nếu bạn muốn chọn 1 hình nền khác cho Web Server, bạn chỉ cần sửa đổi dòng sau (thay đổi tên hình ảnh thành hình bạn muốn). Như hiện tại tên hình ảnh đang là winter.jpg:

background-image: url(winter.jpg);

Đoạn JavaScript được nằm giữa các thẻ <script> và <script>:

<script>
setInterval(updateValues("temperature"), 5000);
setInterval(updateValues("humidity"), 5000);
setInterval(updateValues("pressure"), 5000);
setInterval(updateValues("rssi"), 5000);
setInterval(updateValues("timeAndDate"), 5000);

function updateValues(value) {
  console.log(value);
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById(value).innerHTML = this.responseText;
    }
  };
  xhttp.open("GET", "/" + value, true);
  xhttp.send();
}
</script>

Code trên thiết bị thu LoRa:

// Import Wi-Fi library
#include <WiFi.h>
#include "ESPAsyncWebServer.h"

#include <SPIFFS.h>

//Libraries for LoRa
#include <SPI.h>
#include <LoRa.h>

//Libraries for OLED Display
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Libraries to get time from NTP Server
#include <NTPClient.h>
#include <WiFiUdp.h>

//define the pins used by the LoRa transceiver module
#define SCK 5
#define MISO 19
#define MOSI 27
#define SS 18
#define RST 14
#define DIO0 26

//433E6 for Asia
//866E6 for Europe
//915E6 for North America
#define BAND 866E6

//OLED pins
#define OLED_SDA 4
#define OLED_SCL 15 
#define OLED_RST 16
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Replace with your network credentials
const char* ssid     = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

// Variables to save date and time
String formattedDate;
String day;
String hour;
String timestamp;


// Initialize variables to get and save LoRa data
int rssi;
String loRaMessage;
String temperature;
String humidity;
String pressure;
String readingID;

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST);

// Replaces placeholder with DHT values
String processor(const String& var){
  //Serial.println(var);
  if(var == "TEMPERATURE"){
    return temperature;
  }
  else if(var == "HUMIDITY"){
    return humidity;
  }
  else if(var == "PRESSURE"){
    return pressure;
  }
  else if(var == "TIMESTAMP"){
    return timestamp;
  }
  else if (var == "RRSI"){
    return String(rssi);
  }
  return String();
}

//Initialize OLED display
void startOLED(){
  //reset OLED display via software
  pinMode(OLED_RST, OUTPUT);
  digitalWrite(OLED_RST, LOW);
  delay(20);
  digitalWrite(OLED_RST, HIGH);

  //initialize OLED
  Wire.begin(OLED_SDA, OLED_SCL);
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("LORA SENDER");
}

//Initialize LoRa module
void startLoRA(){
  int counter;
  //SPI LoRa pins
  SPI.begin(SCK, MISO, MOSI, SS);
  //setup LoRa transceiver module
  LoRa.setPins(SS, RST, DIO0);

  while (!LoRa.begin(BAND) && counter < 10) {
    Serial.print(".");
    counter++;
    delay(500);
  }
  if (counter == 10) {
    // Increment readingID on every new reading
    Serial.println("Starting LoRa failed!"); 
  }
  Serial.println("LoRa Initialization OK!");
  display.setCursor(0,10);
  display.clearDisplay();
  display.print("LoRa Initializing OK!");
  display.display();
  delay(2000);
}

void connectWiFi(){
  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  display.setCursor(0,20);
  display.print("Access web server at: ");
  display.setCursor(0,30);
  display.print(WiFi.localIP());
  display.display();
}

// Read LoRa packet and get the sensor readings
void getLoRaData() {
  Serial.print("Lora packet received: ");
  // Read packet
  while (LoRa.available()) {
    String LoRaData = LoRa.readString();
    // LoRaData format: readingID/temperature&soilMoisture#batterylevel
    // String example: 1/27.43&654#95.34
    Serial.print(LoRaData); 
    
    // Get readingID, temperature and soil moisture
    int pos1 = LoRaData.indexOf('/');
    int pos2 = LoRaData.indexOf('&');
    int pos3 = LoRaData.indexOf('#');
    readingID = LoRaData.substring(0, pos1);
    temperature = LoRaData.substring(pos1 +1, pos2);
    humidity = LoRaData.substring(pos2+1, pos3);
    pressure = LoRaData.substring(pos3+1, LoRaData.length());    
  }
  // Get RSSI
  rssi = LoRa.packetRssi();
  Serial.print(" with RSSI ");    
  Serial.println(rssi);
}

// Function to get date and time from NTPClient
void getTimeStamp() {
  while(!timeClient.update()) {
    timeClient.forceUpdate();
  }
  // The formattedDate comes with the following format:
  // 2018-05-28T16:00:13Z
  // We need to extract date and time
  formattedDate = timeClient.getFormattedDate();
  Serial.println(formattedDate);

  // Extract date
  int splitT = formattedDate.indexOf("T");
  day = formattedDate.substring(0, splitT);
  Serial.println(day);
  // Extract time
  hour = formattedDate.substring(splitT+1, formattedDate.length()-1);
  Serial.println(hour);
  timestamp = day + " " + hour;
}

void setup() { 
  // Initialize Serial Monitor
  Serial.begin(115200);
  startOLED();
  startLoRA();
  connectWiFi();
  
  if(!SPIFFS.begin()){
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });
  server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", temperature.c_str());
  });
  server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", humidity.c_str());
  });
  server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", pressure.c_str());
  });
  server.on("/timestamp", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", timestamp.c_str());
  });
  server.on("/rssi", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", String(rssi).c_str());
  });
  server.on("/winter", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/winter.jpg", "image/jpg");
  });
  // Start server
  server.begin();
  
  // Initialize a NTPClient to get time
  timeClient.begin();
  // Set offset time in seconds to adjust for your timezone, for example:
  // GMT +1 = 3600
  // GMT +8 = 28800
  // GMT -1 = -3600
  // GMT 0 = 0
  timeClient.setTimeOffset(0);
}

void loop() {
  // Check if there are LoRa packets available
  int packetSize = LoRa.parsePacket();
  if (packetSize) {
    getLoRaData();
    getTimeStamp();
  }
}

Bây giờ, bạn hãy chèn thông tin mạng WiFi của bạn và lưu bản code này lại. Sau đó, trong Arrduino IDE, bạn mở Sketch >> Show Sketch Folder và tạo 1 thư mục có tên là data. Trong thư mục đó, bạn lưu các file HTML và hình ảnh cần dùng cho Web Server nhé!

Sau khi chọn lưu tất cả các file cần thiết, bạn hãy chọn Tool >> ESP32 Data Sketch Upload. Sau vài giây, các file sẽ được tải lên SPIFFS thành công.

Lưu ý: Nếu không chọn được tùy chọn ESP32 Data Sketch Upload, điều đó có nghĩa là bạn chưa cài đặt plugin hỗ trợ. Tham khảo hướng dẫn cài plugin SPIFFS).

Bây giờ, bạn nạp code vào ESP32 của mình, mở Serial Monitor ở tốc độ 115200 để quan sát cách dự án LoRa ESP32 Arduino hoạt động nhé! Bạn cũng có thể mở Web Server thông qua địa chỉ IP của mạch ESP32 để giám sát thông tin từ xa.

Chúc bạn thành công!

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *