ESP8266 и Arduino IDE. Часть 10a: IoT-сайт. Мониторинг температуры и влажности

Эта статья демонстрирует создание станции мониторинга окружающей среды, которая отображает данные датчиков как на LCD-дисплее, так и на веб-странице через WebSocket.

Готовая веб-страница IoT-монитора

План проекта

Проект будет построен поэтапно:

  1. Тестирование датчиков с базовым скетчем

  2. Создание базового сайта с использованием WebSocket для данных датчиков

  3. Улучшение сайта с циферблатами и графиками

  4. Добавление LCD-дисплея

  5. Интеграция WiFi Manager

  6. Добавление синхронизации времени через NTP

  7. Размещение проекта в корпусе

Эта статья покрывает пункты 1-2, последующие части (10b-10d) охватывают дополнительные функции.


Компоненты схемы

  • NodeMCU V0.9 модуль ESP8266

  • DHT11 датчик температуры/влажности (пин D6)

  • Фоторезистор LDR (пин A0)

  • Индикаторный светодиод (пин D5)

  • Пины I2C D1 и D2 зарезервированы для будущего LCD

NodeMCU модуль

Макетная плата

Макетная плата вид 2

План макетной платы

План макетной платы 2

Принципиальная схема

Скетч 1: Тестирование схемы

Этот скетч тестирует схему и датчики, выводя показания в монитор порта. Для работы требуется библиотека Adafruit DHT (доступна через Library Manager или на GitHub).

//
//  ESP8266 and the Arduino IDE Part 10: Environment monitor station
//  ESP8266-10_sketch01_Circuit_Test
//

#include "DHT.h"
#define DHTPIN D6
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

byte const pin_LDR = A0;
byte const pin_LED = D5;

float humidity = 0;
float tempC = 0;
float tempF = 0;

int brightness = 0;
boolean DHTvaluesOK = false;


void setup()
{
  pinMode(pin_LED, OUTPUT);
  digitalWrite(pin_LED, LOW);
  dht.begin();
  Serial.begin(9600);
  while (!Serial) {;}
  Serial.println("");
  Serial.println("STARTED");
}

void loop()
{
  Serial.println(" ");
  brightness = analogRead(pin_LDR);
  Serial.print("Brightness = ");
  Serial.print(brightness);
  Serial.print("\t");

  humidity = dht.readHumidity();
  tempC = dht.readTemperature();
  tempF = dht.readTemperature(true);

  if (isnan(humidity) || isnan(tempC) || isnan(tempF))
  {
    DHTvaluesOK = false;
  }
  else
  {
    DHTvaluesOK = true;
  }

  if (DHTvaluesOK)
  {
      Serial.print("Humidity: ");
      Serial.print(humidity);
      Serial.print(" %\t");
      Serial.print("Temperature: ");
      Serial.print(tempC);
      Serial.print ("*C  ");
      Serial.print(tempF);
      Serial.println("*F");
  }
  else
  {
      Serial.println("DHT11 ERROR");
  }

  digitalWrite(pin_LED, HIGH);
  delay(100);
  digitalWrite(pin_LED, LOW);

  delay(3000);
}

Монитор порта -- тест датчиков

Скетч 2: Шаблон WebSocket

//
//  ESP8266 and the Arduino IDE Part 10: Environment monitor station
//  ESP8266-10_sketch_02_WebSocket_Template
//

String header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
String html_1 = R"=====(
<!DOCTYPE html>
<html>
<head>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'/>
  <meta charset='utf-8'>
  <style>
    body     { font-size:100%;}
    #main    { display: table; width: 300px; margin: auto;  padding: 10px; border: 3px solid blue; border-radius: 10px; text-align:center;}
    p        { font-size: 100%; }
  </style>
  <title>Websockets</title>
</head>
<body>
  <div id='main'>
    <h3>Websockets</h3>
    <div id='content'>
    </div>
  </div>
</body>

<script>
var Socket;
function init()
{
  Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
  Socket.onmessage = function(event) { processReceivedCommand(event); };
}

function processReceivedCommand(evt)
{
    document.getElementById('rd').textContent = evt.data;
    console.log(evt.data);
}

function sendText(data) { Socket.send(data); }
window.onload = function(e) { init(); }
</script>

</html>
)=====";

#include <ESP8266WiFi.h>
#include <WebSocketsServer.h>

WiFiServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);

char ssid[] = "ssid";
char pass[]= "password";

void setup()
{
  Serial.begin(9600);
  Serial.println();
  Serial.println("Serial started at 9600");
  Serial.println();

  Serial.print("Connecting to ");
  Serial.println("ssid");
  WiFi.begin(ssid,pass);

  int count = 0;
  while ( (WiFi.status() != WL_CONNECTED) && count < 17)
  {
      Serial.print(".");
      delay(500);
      count++;
  }
  Serial.println("");
  if (WiFi.status() != WL_CONNECTED)
  {
     Serial.print("Failed to connect to ");
     Serial.println(ssid);
     while(1);
  }

  Serial.print("Connected to ");
  Serial.println(WiFi.localIP());

  server.begin();
  Serial.println("Server started");

  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
}

void loop()
{
    webSocket.loop();

    WiFiClient client = server.available();
    if (client)
    {
        client.flush();
        client.print( header );
        client.print( html_1 );
        Serial.println("New page served");
    }
}

void webSocketEvent(byte num, WStype_t type, uint8_t * payload, size_t length)
{
    Serial.println();
    Serial.print("WStype = ");
    Serial.println(type);
    Serial.print("WS payload = ");
    for(int i = 0; i < length; i++) {
      Serial.print((char) payload[i]);
    }
    Serial.println();
}

HTML-шаблон веб-страницы

Шаблон использует элементы <span> с уникальными ID для адресного обновления значений. Символ &#176; представляет знак градуса.

Шаблон веб-страницы

Скетч 3: Базовый сайт с интеграцией датчиков

//
//  ESP8266 and the Arduino IDE Part 10: Environment monitor station
//  ESP8266-10_sketch03_Basic_Website
//

String header = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
String html_1 = R"=====(
<!DOCTYPE html>
<html>
<head>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'/>
  <meta charset='utf-8'>
  <style>
    body     { font-size:100%;}
    #main    { display: table; width: 300px; margin: 20px auto; padding: 10px; border: 1px solid blue; border-radius: 10px; text-align:center;}
    p        { font-size: 120%; }
  </style>
  <title>ESP8266 Part 10</title>
</head>
<body>
  <div id='main'>
    <h1>ESP8266 monitor</h1>
    <div id='content'>
      <p>Humidity = <span id='humidity'>00</span>%</p>
      <p>Temperature = <span id='tempC'>00</span>&#176;C</p>
      <p>Temperature = <span id='tempF'>00</span>&#176;F</p>
      <p>Brightness = <span id='brightness'>00</span></p>
      <p>Data = <span id='recData'>00</span></p>
    </div>
    <br />
   </div>
</body>
<script>
  var Socket;
  function init()
  {
    Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
    Socket.onmessage = function(event) { processReceivedCommand(event); };
  }

function processReceivedCommand(evt)
{
    var data = evt.data;
    document.getElementById('recData').textContent = data;
    var tmp = data.split('|');
    document.getElementById('humidity').textContent = tmp[0];
    document.getElementById('tempC').textContent = tmp[1];
    document.getElementById('tempF').textContent = tmp[2];
    document.getElementById('brightness').textContent = tmp[3];
}

function sendText(data)       { Socket.send(data);   }
window.onload = function(e)   { init();  }
</script>
</html>
)=====";

#include <ESP8266WiFi.h>
#include <WebSocketsServer.h>

WiFiServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);

char ssid[] = "mySSID";
char pass[]= "myPassword";

#include "DHT.h"
#define DHTPIN D6
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

byte const pinLED = D5;
byte const pinLDR = A0;

int brightness = 0;
float humidity = 0;
float tempC = 0;
float tempF = 0;

boolean DHTreadingsOK = false;
boolean updateWebpage = false;

long sensorUpdateFrequency = 5000;
long timeNow = 0;
long timePrev = 0;

void setup()
{
  pinMode(pinLED, OUTPUT);
  digitalWrite(pinLED, LOW);
  dht.begin();

  Serial.begin(9600);
  while (!Serial) {;}
  Serial.println();
  Serial.println("Serial started at 9600");
  Serial.println();

  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid,pass);

  int count = 0;
  digitalWrite(pinLED, HIGH);
  while ( (WiFi.status() != WL_CONNECTED) && count < 17)
  {
      Serial.print(".");
      count++;
      delay(500);
  }
  digitalWrite(pinLED, LOW);

  if (WiFi.status() != WL_CONNECTED)
  {
     Serial.println("");
     Serial.print("Failed to connect to ");
     Serial.println(ssid);
     while(1);
  }

  Serial.println("");
  Serial.print("Connected. IP address = ");
  Serial.println(WiFi.localIP());

  server.begin();
  Serial.println("Server started");

  webSocket.begin();
  Serial.println("websocket started");

  Serial.println("");
  Serial.println("");

  webSocket.onEvent(webSocketEvent);
}

void loop()
{
    webSocket.loop();

    WiFiClient client = server.available();

    if (client)
    {
        client.flush();
        client.print( header );
        client.print( html_1 );
        Serial.println("New page served");
        Serial.println("");
    }

    timeNow = millis();
    if (timeNow - timePrev >= sensorUpdateFrequency)
    {
      timePrev = timeNow;
      updateSensors();
    }
}

void updateSensors()
{
    brightness = analogRead(pinLDR);
    humidity = dht.readHumidity();
    tempC = dht.readTemperature();
    tempF = dht.readTemperature(true);

    if (isnan(humidity) || isnan(tempC) || isnan(tempF))
    {
       Serial.println("Error reading from the DHT11.");
    }
   else
   {
      String data = "";
      data = String(data + byte(humidity) );
      data = String(data + "|");
      data = String(data + tempC);
      data = String(data + "|");
      data = String(data + tempF);
      data = String(data + "|");
      data = String(data + brightness);

      webSocket.broadcastTXT(data);
      Serial.println(data);

      digitalWrite(pinLED, HIGH);
      delay(50);
      digitalWrite(pinLED, LOW);
  }
}

void webSocketEvent(byte num, WStype_t type, uint8_t * payload, size_t length)
{
    Serial.println("");
    Serial.print("WStype = ");
    Serial.println(type);
    Serial.print("WS payload = ");
    for(int i = 0; i < length; i++) {
      Serial.print((char) payload[i]);
    }
    Serial.println();
}

Ключевые особенности:

  • Используется таймер на основе millis() вместо delay() для неблокирующего обновления датчиков

  • Данные отправляются в формате, разделённом вертикальной чертой: humidity|tempC|tempF|brightness

  • JavaScript разделяет полученные данные в массив для обновления DOM-элементов

  • Светодиод мигает при передаче данных

Монитор порта

Базовая веб-страница

Монитор порта с данными

Важные технические концепции

WebSocket vs традиционный опрос: При использовании WebSocket после первоначальной отдачи веб-страницы все последующие обновления могут проходить через WebSocket, и может не потребоваться отдача другой страницы.

Формат данных: Формат значений, разделённых вертикальной чертой, позволяет методу JavaScript split() разбирать строку в массив без необходимости именованных идентификаторов.

Неблокирующие обновления: Использование millis() вместо delay() позволяет скетчу обрабатывать несколько задач одновременно (клиентские подключения и считывание датчиков).

Использование String: Строки (String) обычно нежелательны. Они потребляют память и могут вызвать различные проблемы, однако на ESP8266 памяти относительно много.