ESP32 LoRa: мониторинг датчиков с веб-сервером (дальняя связь)

В этом проекте вы создадите систему мониторинга датчиков с использованием платы TTGO LoRa32 SX1276 OLED, которая отправляет показания температуры, влажности и давления по радио LoRa на приёмник ESP32 LoRa. Приёмник отображает последние показания датчиков на веб-сервере.

ESP32 LoRa Sensor Monitoring with Web Server Long Range Communication

В этом проекте вы научитесь:

  • Отправлять показания датчиков по радио LoRa между двумя платами ESP32;

  • Добавлять возможности LoRa и Wi-Fi одновременно в ваши проекты (LoRa + веб-сервер на одной плате ESP32);

  • Использовать плату TTGO LoRa32 SX1276 OLED или аналогичные платы разработки для проектов IoT.

Рекомендуемое чтение: TTGO LoRa32 SX1276 OLED Board: Getting Started with Arduino IDE

Видео-демонстрация

Посмотрите видео-демонстрацию, чтобы увидеть, что вы будете создавать в этом руководстве.

Обзор проекта

На следующем изображении показан общий обзор проекта, который мы создадим в этом руководстве.

Project Overview ESP32 LoRa Sender and ESP32 LoRa32 Receiver board
  • Передатчик LoRa отправляет показания датчика BME280 по радио LoRa каждые 10 секунд;

  • Приёмник LoRa получает показания и отображает их на веб-сервере;

  • Вы можете отслеживать показания датчиков, обращаясь к веб-серверу;

  • Передатчик LoRa и приёмник LoRa могут находиться на расстоянии нескольких сотен метров друг от друга в зависимости от их расположения. Таким образом, вы можете использовать этот проект для мониторинга показаний датчиков с ваших полей или теплиц, если они находятся на некотором удалении от вашего дома;

  • Приёмник LoRa работает на асинхронном веб-сервере, а файлы веб-страницы сохраняются в файловой системе ESP32 (LittleFS);

  • Приёмник LoRa также показывает дату и время получения последних показаний. Для получения даты и времени мы используем Протокол сетевого времени (NTP) с ESP32.

Для введения в связь LoRa: что такое LoRa, частоты LoRa, применение LoRa и многое другое, прочитайте нашу статью Getting Started ESP32 with LoRa using Arduino IDE.

Необходимые компоненты

TTGO LoRa32 SX1276 OLED board with antenna

Для этого проекта мы будем использовать следующие компоненты:

Вам также понадобятся соединительные провода и макетная плата.

Вы можете использовать ссылки выше или перейти непосредственно на MakerAdvisor.com/tools чтобы найти все компоненты для ваших проектов по лучшей цене!

Подготовка Arduino IDE

Для программирования плат TTGO LoRa32 SX1276 OLED мы будем использовать Arduino IDE. Для загрузки файлов в файловую систему ESP32 мы будем использовать плагин ESP32 LittleFS.

Поэтому, прежде чем продолжить, вам нужно установить платы ESP32 и плагин файловой системы ESP32 в вашу Arduino IDE.

Установка библиотек

Для этого проекта вам нужно установить несколько библиотек.

Библиотеки LoRa, BME280, OLED и веб-сервера

Следующие библиотеки можно установить через Arduino Library Manager. Перейдите в Sketch > Include Library > Manage Libraries и найдите библиотеки по имени.

Библиотека NTPClient

Каждый раз, когда приёмник LoRa получает новое сообщение LoRa, он запрашивает дату и время с NTP-сервера, чтобы мы знали, когда был получен последний пакет.

Для этого мы будем использовать библиотеку NTPClient, форк от Taranais. Выполните следующие шаги для установки этой библиотеки в Arduino IDE:

ВАЖНО: мы не используем стандартную библиотеку NTPClient. Чтобы следовать этому руководству, вам нужно установить библиотеку, которую мы рекомендуем, используя следующие шаги.

  1. Нажмите здесь, чтобы скачать библиотеку NTP Client. У вас должна быть .zip папка в папке Downloads

  2. В вашей Arduino IDE перейдите в Sketch > Include Library > Add .ZIP library…

  3. Выберите .ZIP файл библиотеки, который вы только что скачали.

  4. Библиотека будет установлена через несколько секунд.

Передатчик LoRa

Передатчик LoRa подключён к датчику BME280 и отправляет показания температуры, влажности и давления каждые 10 секунд. Вы можете изменить этот период времени позже в коде.

Рекомендуемое чтение: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity)

Схема подключения передатчика LoRa

Датчик BME280, который мы используем, взаимодействует с ESP32 по протоколу I2C. Подключите датчик, как показано на следующей схеме:

TTGO LoRa32 SX1276 OLED board ESP32 Sender

BME280

ESP32

VIN

3.3 V

GND

GND

SCL

GPIO 13

SDA

GPIO 21

Код передатчика LoRa

Следующий код считывает температуру, влажность и давление с датчика BME280 и отправляет показания по радио LoRa.

Скопируйте следующий код в вашу Arduino IDE.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-lora-sensor-web-server/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/

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

Посмотреть исходный код

Как работает код

Начните с подключения необходимых библиотек для LoRa, OLED-дисплея и датчика BME280.

//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>

Определите пины, используемые модулем приёмопередатчика LoRa. Мы используем плату TTGO LoRa32 SX1276 OLED V1.0, и это пины, используемые чипом LoRa:

//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

Примечание: если вы используете другую плату LoRa, проверьте пины, используемые чипом приёмопередатчика LoRa.

Выберите частоту LoRa:

#define BAND 866E6

Определите пины OLED.

#define OLED_SDA 4
#define OLED_SCL 15
#define OLED_RST 16

Определите размер OLED.

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

Определите пины, используемые датчиком BME280.

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

Создайте экземпляр I2C для датчика BME280 и объект bme.

TwoWire I2Cone = TwoWire(1);
Adafruit_BME280 bme;

Создайте несколько переменных для хранения сообщения LoRa, температуры, влажности, давления и ID чтения.

int readingID = 0;

int counter = 0;
String LoRaMessage = "";

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

Создайте объект display для OLED-дисплея.

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

setup()

В setup() мы вызываем несколько функций, которые были созданы ранее в коде, для инициализации OLED-дисплея, BME280 и модуля приёмопередатчика LoRa.

void setup() {
  Serial.begin(115200);
  startOLED();
  startBME();
  startLoRA();
}

loop()

В loop() мы вызываем функции getReadings() и sendReadings(), которые также были созданы ранее. Эти функции отвечают за получение показаний с датчика BME280 и отправку этих показаний по LoRa соответственно.

void loop() {
  getReadings();
  sendReadings();
  delay(10000);
}

getReadings()

Получение показаний датчика так же просто, как использование методов readTemperature(), readHumidity() и readPressure() объекта bme:

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

sendReadings()

Для отправки показаний по LoRa мы объединяем все показания в одну переменную LoRaMessage:

void sendReadings() {
  LoRaMessage = String(readingID) + "/" + String(temperature) + "&" + String(humidity) + "#" + String(pressure);

Обратите внимание, что каждое показание разделено специальным символом, чтобы приёмник мог легко идентифицировать каждое значение.

Затем отправьте пакет, используя следующее:

LoRa.beginPacket();
LoRa.print(LoRaMessage);
LoRa.endPacket();

Каждый раз, когда мы отправляем пакет LoRa, мы увеличиваем переменную readingID, чтобы иметь представление о том, сколько пакетов было отправлено. Вы можете удалить эту переменную, если хотите.

readingID++;

Цикл loop() повторяется каждые 10000 миллисекунд (10 секунд). Таким образом, новые показания датчика отправляются каждые 10 секунд. Вы можете изменить это время задержки, если хотите.

delay(10000);

Тестирование передатчика LoRa

Загрузите код на вашу плату ESP32 LoRa Sender.

Перейдите в Tools > Port и выберите COM-порт, к которому она подключена. Затем перейдите в Tools > Board и выберите используемую плату. В нашем случае это TTGO LoRa32-OLED V1.

Arduino IDE selecting TTGO LoRa32-OLED-V1 board

Нажмите кнопку загрузки.

Arduino IDE Upload button

Откройте Serial Monitor на скорости 115200 бод. Вы должны получить что-то похожее на показанное ниже.

Arduino IDE: ESP32 LoRa Sender Circuit Demonstration

OLED вашей платы должен отображать последние показания датчика.

TTGO LoRa32 SX1276 OLED board ESP32 Sender Circuit Schematic

Ваш передатчик LoRa готов. Теперь перейдём к приёмнику LoRa.

Приёмник LoRa

Приёмник LoRa получает входящие пакеты LoRa и отображает полученные показания на асинхронном веб-сервере. Помимо показаний датчиков, мы также отображаем время последнего получения этих показаний и RSSI (индикатор уровня принятого сигнала).

На следующем рисунке показан веб-сервер, который мы создадим.

TTGO LoRa32 board ESP32 Receiver Web Server Example

Как видите, он содержит фоновое изображение и стили, чтобы сделать веб-страницу более привлекательной. Существует несколько способов отображения изображений на веб-сервере ESP32. Мы сохраним изображение в файловой системе ESP32 (LittleFS). Мы также сохраним HTML-файл в LittleFS.

Организация файлов

Для создания веб-сервера вам понадобятся три разных файла: скетч Arduino, HTML-файл и изображение. HTML-файл и изображение должны быть сохранены внутри папки с именем data внутри папки скетча Arduino, как показано ниже.

ESP32 Filesystem plugin files structure organized data folder HTML jpg

Создание HTML-файла

Создайте файл index.html со следующим содержимым или скачайте все файлы проекта здесь:

<!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>

Посмотреть исходный код

Мы также включили CSS-стили в HTML-файл, а также немного JavaScript, который отвечает за автоматическое обновление показаний датчиков.

Важно обратить внимание на плейсхолдеры (заполнители). Плейсхолдеры заключены в знаки %: %TIMESTAMP%, %TEMPERATURE%, %HUMIDITY%, %PRESSURE% и %RSSI%.

Эти плейсхолдеры затем будут заменены фактическими значениями кодом Arduino.

Стили добавлены между тегами <style> и </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>

Если вы хотите другое изображение для фона, вам просто нужно изменить следующую строку, чтобы включить имя вашего изображения. В нашем случае оно называется winter.jpg.

background-image: url(winter.jpg);

JavaScript находится между тегами <script> и </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>

Мы не будем подробно объяснять, как работает HTML и CSS, но хорошее место для изучения — сайт W3Schools.

Скетч Arduino для приёмника LoRa

Скопируйте следующий код в вашу Arduino IDE или скачайте все файлы проекта здесь. Затем вам нужно ввести ваши сетевые учётные данные (SSID и пароль), чтобы это работало.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-lora-sensor-web-server/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
// Import Wi-Fi library
#include <WiFi.h>
#include "ESPAsyncWebServer.h"

#include <LittleFS.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(!LittleFS.begin()){
    Serial.println("An Error has occurred while mounting LittleFS");
    return;
  }
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/index.html", String(), false, processor);
  });
  server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", temperature.c_str());
  });
  server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", humidity.c_str());
  });
  server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", pressure.c_str());
  });
  server.on("/timestamp", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", timestamp.c_str());
  });
  server.on("/rssi", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", String(rssi).c_str());
  });
  server.on("/winter", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/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();
  }
}

Посмотреть исходный код

Как работает код

Начните с подключения необходимых библиотек. Вам нужны библиотеки для:

  • построения асинхронного веб-сервера;

  • доступа к файловой системе ESP32 (LittleFS);

  • связи с чипом LoRa;

  • управления OLED-дисплеем;

  • получения даты и времени с NTP-сервера.

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

#include <LittleFS.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>

Определите пины, используемые модулем приёмопередатчика LoRa.

#define SCK 5
#define MISO 19
#define MOSI 27
#define SS 18
#define RST 14
#define DIO0 26

Примечание: если вы используете другую плату LoRa, проверьте пины, используемые чипом приёмопередатчика LoRa.

Определите частоту LoRa:

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

Настройте пины OLED:

#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

Введите ваши сетевые учётные данные в следующие переменные, чтобы ESP32 мог подключиться к вашей локальной сети.

const char* ssid     = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Определите NTP-клиент для получения даты и времени:

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

Создайте переменные для сохранения даты и времени:

String formattedDate;
String day;
String hour;
String timestamp;

Ещё переменные для хранения показаний датчиков, полученных по радио LoRa.

int rssi;
String loRaMessage;
String temperature;
String humidity;
String pressure;
String readingID;

Создайте объект AsyncWebServer с именем server на порту 80.

AsyncWebServer server(80);

Создайте объект display для OLED-дисплея:

AsyncWebServer server(80);

processor()

Функция processor() – это то, что присваивает значения плейсхолдерам, которые мы создали в HTML-файле.

Она принимает в качестве аргумента плейсхолдер и должна возвращать String, который заменит этот плейсхолдер.

Например, если она находит плейсхолдер TEMPERATURE, она вернёт строковую переменную temperature.

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

setup()

В setup() вы инициализируете OLED-дисплей, связь LoRa и подключаетесь к Wi-Fi.

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  startOLED();
  startLoRA();
  connectWiFi();

Вы также инициализируете LittleFS:

if(!LittleFS.begin()){
  Serial.println("An Error has occurred while mounting LittleFS");
  return;
}

Асинхронный веб-сервер

Библиотека ESPAsyncWebServer позволяет нам настраивать маршруты, по которым сервер будет прослушивать входящие HTTP-запросы.

Например, когда получен запрос по корневому URL, мы отправляем файл index.html, сохранённый в ESP32 LittleFS:

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(LittleFS, "/index.html", String(), false, processor);
});

Как упоминалось ранее, мы добавили немного JavaScript в HTML-файл, который отвечает за обновление веб-страницы каждые 10 секунд. Когда это происходит, он делает запрос по URL /temperature, /humidity, /pressure, /timestamp, /rssi.

Итак, нам нужно обработать, что происходит, когда мы получаем эти запросы. Нам просто нужно отправить переменные temperature, humidity, pressure, timestamp и rssi. Переменные должны быть отправлены в формате char, поэтому мы используем метод .c_str().

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

Поскольку мы включили изображение в веб-страницу, мы получим запрос, «запрашивающий» изображение. Поэтому нам нужно отправить изображение, сохранённое в ESP32 LittleFS.

server.on("/winter", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(LittleFS, "/winter.jpg", "image/jpg");
});

Наконец, запустите веб-сервер.

server.begin();

NTPClient

Ещё в setup() создайте NTP-клиент для получения времени из интернета.

timeClient.begin();

Время возвращается в формате GMT, поэтому, если вам нужно настроить для вашего часового пояса, вы можете использовать следующее:

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

loop()

В loop() мы прослушиваем входящие пакеты LoRa:

int packetSize = LoRa.parsePacket();

Если доступен новый пакет LoRa, мы вызываем функции getLoRaData() и getTimeStamp().

if (packetSize) {
  getLoRaData();
  getTimeStamp();
}

Функция getLoRaData() получает сообщение LoRa и разделяет его для получения различных показаний.

Функция getTimeStamp() получает время и дату из интернета в момент получения пакета.

Загрузка кода и файлов

После ввода ваших сетевых учётных данных сохраните скетч. Затем в вашей Arduino IDE перейдите в Sketch > Show Sketch Folder и создайте папку с именем data.

Arduino IDE Open Sketch Folder to create data folder

Внутри этой папки должны быть HTML-файл и файл изображения.

Убедившись, что все необходимые файлы находятся в правильных каталогах, вам нужно загрузить файлы в файловую систему ESP32 LittleFS.

Нажмите [Ctrl] + [Shift] + [P] в Windows или [Cmd] + [Shift] + [P] в MacOS, чтобы открыть палитру команд. Найдите команду Upload LittleFS to Pico/ESP8266/ESP32 и нажмите на неё.

Если у вас нет этой опции, значит вы не установили плагин загрузчика файловой системы. Проверьте это руководство.

ESP32 Sketch Data Upload LittleFS Arduino IDE

Важно: убедитесь, что Serial Monitor закрыт перед загрузкой в файловую систему. В противном случае загрузка не удастся.

Через несколько секунд файлы должны быть успешно загружены в LittleFS.

Теперь загрузите скетч на вашу плату.

Arduino IDE 2 Upload Button

Откройте Serial Monitor на скорости 115200 бод.

Вы должны получить IP-адрес ESP32, и вы должны начать получать пакеты LoRa от передатчика.

ESP32 Arduino IDE Serial Monitor window

Вы также должны увидеть IP-адрес, отображаемый на OLED.

TTGO LoRa32 SX1276 OLED board ESP32 Receiver Circuit Schematic web server

Демонстрация

Откройте браузер и введите IP-адрес вашего ESP32. Вы должны увидеть веб-сервер с последними показаниями датчиков.

ESP32 LoRa + Web Server + Sensor readings

С этими платами нам удалось получить стабильную связь LoRa на расстоянии до 180 метров (590 футов) на открытом поле. Это означает, что мы можем расположить передатчик и приёмник на расстоянии 180 метров друг от друга и всё ещё иметь возможность получать и проверять показания на веб-сервере.

LoRa32 SX1276 OLED Board Communication Range Experiment

Получение стабильной связи на расстоянии 180 метров с такими дешёвыми платами и без дополнительных настроек действительно впечатляет.

Однако в предыдущем проекте, используя чип приёмопередатчика RFM95 SX1276 LoRa с самодельной антенной, мы получили лучшие результаты: более 250 метров с множеством препятствий между ними.

RFM95 LoRa SX1276 transceiver chip connected to an ESP32

Дальность связи будет действительно зависеть от вашей среды, используемой платы LoRa и многих других переменных.

Заключение

Вы можете развить этот проект дальше и создать автономную систему мониторинга, добавив солнечные панели и глубокий сон к вашему передатчику LoRa. Следующие статьи могут вам в этом помочь:

Вы также можете захотеть получить доступ к показаниям датчиков из любой точки мира или отобразить их на графике:

Мы надеемся, что этот проект показался вам интересным.

Спасибо за чтение.

Источник: Rui Santos & Sara Santos – Random Nerd Tutorials