ESP32:Примеры/WiFi-мультидатчик на базе ESP32: температура, влажность, движение, яркость и управление реле

Материал из Онлайн справочника
Версия от 22:33, 31 января 2019; Myagkij (обсуждение | вклад) (→‎=)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Черновик


WiFi-мультидатчик на базе ESP32: температура, влажность, движение, яркость и управление реле

В этом примере мы покажем, как создать систему на базе ESP32, оснащенную несколькими датчиками и коммуницирующую через WiFi. Она будет состоять из PIR-датчика движения, фоторезистора, датчика температуры и влажности DHT22, реле и статусного RGB-светодиода.

Мы также создадим веб-сервер, позволяющий мониторить данные всех этих датчиков, а также управлять реле при помощи нескольких разных режимов.

Чтобы лучше понимать некоторые концепты, используемые в этом проекте, рекомендуем ознакомиться с этими руководствами:

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

Веб-сервер для этого проекта позволит вам выбирать между 4 режимами управления реле:

  • Ручной режим или Manual (режим 0). В этом режиме вы сможете вручную включать/выключать реле при помощи кнопки на веб-странице.
  • Режим PIR-датчика или Auto PIR (режим 1). Реле будет включаться при обнаружении движения. В этом режиме на веб-странице будет поле, где можно задать количество секунд, на которые нужно включить реле после обнаружения движения.
  • Режим фоторезистора или Auto LDR (режим 2). Реле будет включаться, когда яркость упадет ниже заданного порога. В этом режиме на веб-странице будет поле, где можно будет задать этот порог (между 0% и 100%).
  • Режим PIR-датчика и фоторезистора или Auto PIR and LDR (режим 3). Как понятно из названия, этот режим объединяет в себе PIR-датчик движения и фоторезистор. Реле будет включаться при выполнении двух условий – если PIR-датчик обнаружит движение и если яркость упадет ниже заданного порога. В этом режиме на веб-странице будет два поля – для таймера и порога яркости фоторезистора.

Статусный индикатор

Наша система также будет оснащена RGB-светодиодом, меняющим цвет в зависимости от выбранного режима:

  • Ручной режим – красный цвет;
  • Режим PIR-датчика – зеленый цвет;
  • Режим фоторезистора – синий цвет;
  • Режим PIR-датчика и фоторезистора – фиолетовый цвет;

Данные от датчиков

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

Кроме того, эти данные можно будет убрать с экрана при помощи кнопки Remove Sensor Readings («Спрятать данные от датчика»), чтобы оптимизировать производительность веб-сервера.

В любом режиме под заголовком страницы будет находиться надпись, подсказывающая, какой режим активен прямо сейчас, а ниже (под выпадающим меню для выбора режима) – текущее состояние выходного контакта, к которому подключено реле.

Кроме того, для этого проекта нам нужно будет, чтобы ESP32 помнила последнее состояние выходного контакта и другие настройки – на случай сброса или внезапного отключения питания. Поэтому мы будем записывать все эти данные в flash-память ESP32.

Примечание: О том, как записывать данные в энергонезависимую flash-память ESP32, читайте в руководстве «Как запомнить последнее состояние контакта».

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

Перед загрузкой кода нам нужно установить в IDE Arduino две библиотеки – «DHT sensor» и «Adafruit Sensor». Они позволят без труда считывать данные с датчика DHT22.

Устанавливаем библиотеку «DHT sensor»

  1. Кликните тут, чтобы скачать ZIP-архив библиотеки. Он должен загрузиться в папку «Загрузки»;
  2. Распакуйте этот ZIP-архив. У вас должна получиться папка «DHT-sensor-library-master»;
  3. Переименуйте эту папку на «DHT»;
  4. Переместите папку «DHT» в папку «libraries» (туда устанавливаются все библиотеки IDE Arduino), которая находится внутри папки, куда установлена ваша IDE Arduino;
  5. Перезапустите IDE Arduino;

Устанавливаем библиотеку «Adafruit Sensor»

  1. Кликните тут, чтобы скачать ZIP-архив библиотеки. Он должен загрузиться в папку «Загрузки»;
  2. Распакуйте этот ZIP-архив. У вас должна получиться папка «Adafruit_Sensor-master»;
  3. Переименуйте эту папку на «Adafruit_Sensor»;
  4. Переместите папку «Adafruit_Sensor» в папку «libraries» (туда устанавливаются все библиотеки IDE Arduino), которая находится внутри папки, куда установлена ваша IDE Arduino;
  5. Перезапустите IDE Arduino.

Что нужно сделать перед загрузкой кода

Вставьте в двух строчках ниже SSID и пароль для своей WiFi-сети:

// вставьте внизу SSID и пароль для своей WiFi-сети:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Вставив SSID и пароль, загрузите этот код на ESP32 (перед этим убедитесь, что в IDE Arduino выбраны правильная плата и COM-порт).

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

Подключаем библиотеки

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

#include <WiFi.h>
#include <EEPROM.h>
#include "DHT.h"
#include <Adafruit_Sensor.h>

Задаем учетные данные для подключения к WiFi-сети

В этих двух переменных необходимо задать SSID и пароль для своей WiFi-сети.

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

Задаем датчик DHT

В следующем фрагменте выбираем тип датчика DHT. В нашем случае выбран DHT22.

//#define DHTTYPE DHT11 // DHT 11 
//#define DHTTYPE DHT21 // DHT 21 (AM2301) 
#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321

Затем задаем контакт, к которому подключен датчик DHT, и создаем объект для него.

// задаем контакт, к которому подключен DHT-датчик:
const int DHTPin = 27;
// инициализируем DHT-датчик:
DHT dht(DHTPin, DHTTYPE);

Далее задаем вспомогательные переменные для хранения данных о температуре (в градусах Цельсия и Фаренгейта) и влажности.

static char celsiusTemp[7]; 
static char fahrenheitTemp[7]; 
static char humidityTemp[7];

Задаем размер flash-памяти

Затем задаем размер flash-памяти, к которой хотим получить доступ.

#define EEPROM_SIZE 4

Нам нужно будет сохранить в flash-память 4 значения: последнее состояние контакта для реле (адрес 0), выбранный режим (адрес 1), значение для таймера (адрес 2) и пороговое значение для фоторезистора (адрес 3). Таким образом, нам понадобится 4 байта flash-памяти.

  • Адрес 0: Последнее состояние контакта реле («0» = выкл, «1» = вкл);
  • Адрес 1: Выбранный режим («0» = Manual, «1» = Auto PIR, «2» = Auto LDR, «3» = Auto PIR and LDR);
  • Адрес 2: Таймер (время в секундах между «0» и «255»);
  • Адрес 3: Пороговое значение для фоторезистора (яркость в процентах между «0» и «100»).

Более подробно о flash-памяти ESP32 можно почитать в руководстве «Как запомнить последнее состояние контакта».

Задаем GPIO-контакты

В этом фрагменте мы задаем, какие GPIO-контакты будут использоваться для реле, RGB-светодиода, PIR-датчика движения и фоторезистора.

const int output = 2;
const int redRGB = 14;
const int greenRGB = 12;
const int blueRGB = 13;
const int motionSensor = 25;
const int ldr = 33;

Также создаем строковую переменную «outputState» для хранения значения контакта, к которому подключено реле. Это значение будет показано на веб-странице.

String outputState = "off";

Задаем таймеры

Далее создаем вспомогательные переменные для таймеров:

long now = millis(); 
long lastMeasure = 0; 
boolean startTimer = false;

Примечание: Более подробно о прерываниях и таймерах в работе с ESP32 можно почитать в руководстве «Использование ESP32 вместе с PIR-датчиком движения».

Задаем переменные для режима и настроек

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

int selectedMode = 0;
int timer = 0;
int ldrThreshold = 0;
int armMotion = 0;
int armLdr = 0;
String modes[4] = { "Manual", "Auto PIR", "Auto LDR", "Auto PIR and LDR" };

Задаем переменные для веб-сервера

Следующий фрагмент кода относится к веб-серверу.

// переменные для декодирования HTTP-запроса GET:
String valueString = "0";
int pos1 = 0;
int pos2 = 0;
// переменная для хранения HTTP-запроса:
String header;
// создаем объект сервера и задаем ему номер порта «80»:
WiFiServer server(80);

Более подробно о веб-серверах можно почитать в руководстве «Веб-сервер на базе ESP32: управление выходными контактами».

Блок setup()

В блоке setup() сначала инициализируем датчик DHT.

dht.begin();

Далее инициализируем последовательную коммуникацию на скорости 115200 бод (для отладочных целей).

Прерывания

Переключаем контакт, к которому подключен PIR-датчик движения, в режим «INPUT_PULLUP», а также задаем для этого контакта прерывание в режиме «RISING».

pinMode(motionSensor, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);

Flash-память

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

  Serial.println("start...");  //  "запуск..."
  if(!EEPROM.begin(EEPROM_SIZE)) {
    Serial.println("failed to initialise EEPROM");
               //  "не удалось инициализировать EEPROM"
    delay(1000);
  }
  
  // раскомментируйте строчки ниже, чтобы проверить,
  // сохранились ли данные на flash-память:
  Serial.println(" bytes read from Flash . Values are:");
             //  " с flash-памяти считаны следующие данные:"
  for(int i = 0; i < EEPROM_SIZE; i++) {
    Serial.print(byte(EEPROM.read(i))); 
    Serial.print(" ");
  }

RGB-светодиод

В этом фрагменте мы делаем выходными (OUTPUT) контакты, к которым подключены реле и ножки RGB-светодиода:

pinMode(output, OUTPUT);
pinMode(redRGB, OUTPUT);
pinMode(greenRGB, OUTPUT);
pinMode(blueRGB, OUTPUT);

Считываем данные с flash-памяти

Далее считываем данные с flash-памяти и записываем в переменную «outputState» самое последнее сохраненное значение контакта, к которому подключено реле. Это значение хранится на адресе «0», поэтому для его чтения нужно воспользоваться функцией EEPROM.read(0).

То есть мы проверяем, что это за значение – «1» или «0» – чтобы потом записать в строковую переменную «outputState» значение «on» или «off». Это строковое значение будет затем показано на веб-странице в браузере.

if(!EEPROM.read(0)) {
outputState = "off";
digitalWrite(output, HIGH);
}
else {
 outputState = "on";
 digitalWrite(output, LOW);
}

Также обновляем переменные, где хранятся все остальные значения, записываемые на flash-память – данные о выбранном режиме, таймере и пороговом значении для фоторезистора.

selectedMode = EEPROM.read(1);
timer = EEPROM.read(2);
ldrThreshold = EEPROM.read(3);

Затем вызываем функцию configureMode(), чтобы задать правильные значения для каждого режима.

configureMode();

Функция configureMode()

Давайте разберем, как работает эта функция.

К примеру, если пользователь выбрал ручной режим (Manual), у нас не будет работать ни PIR-датчик движения («armMotion»), ни фоторезистор («armLdr»). Но нам нужно переключить RGB-светодиод в красный цвет.

// ручной режим:
if(selectedMode == 0) {
  armMotion = 0;
  armLdr = 0;
  // цвет RGB-светодиода - красный:
  digitalWrite(redRGB, LOW);
  digitalWrite(greenRGB, HIGH);
  digitalWrite(blueRGB, HIGH);
}

Аналогичным образом задаются настройки и для других режимов. Мы меняем значения в переменных «armMotion» и «armLdr» для активации/деактивации PIR-датчика движения и фоторезистора, а также задаем значения для ножек RGB-светодиода, чтобы переключить его в цвет, соответствующий выбранному режиму.

Теперь давайте вернемся в блок setup().

Подключаемся к WiFi

В этом блоке мы подключаемся к WiFi-сети и печатаем в мониторе порта IP-адрес ESP32.

// подключаемся к WiFi-сети при помощи заданных выше SSID и пароля: 
Serial.print("Connecting to ");  //  "Подключаемся к "
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
}
// печатаем локальный IP-адрес и запускаем веб-сервер:
Serial.println("");
Serial.println("WiFi connected.");  //  "Подключились к WiFi."
Serial.println("IP address: ");  //  "IP-адрес: "
Serial.println(WiFi.localIP());
server.begin();

Блок loop()

В блоке loop() мы показываем в браузере страницу веб-сервера, а также выполняем действия, соответствующие выбранному режиму и его настройкам.

В выпадающем меню, где написано «Change mode» («Переключить режим»), можно выбрать один из следующих вариантов:

Manual (Ручной режим)

К примеру, если выбрать ручной режим (Manual), будет выполнен вот этот фрагмент кода:

if(header.indexOf("GET /?mode=") >= 0) {
    pos1 = header.indexOf('=');
    pos2 = header.indexOf('&');
    valueString = header.substring(pos1+1, pos2);
    selectedMode = valueString.toInt();
    EEPROM.write(1, selectedMode);
    EEPROM.commit();
    configureMode();
}

Этот фрагмент сохраняет значение, соответствующее выбранному режиму, в переменную «selectedMode» и на flash-память в адрес «1» при помощи EEPROM.write(1, selectedMode);.

Внешний вид веб-страницы изменится согласно выбранному режиму. В нашем случае – то есть поскольку мы выбрали ручной режим (0) – правдивым будет условие, следующее ниже, и в результате на веб-странице будет показана одна из двух кнопок: «ON» или «OFF». С помощью этой кнопки мы можем поменять значение контакта, к которому подключено реле.

if(selectedMode == 0) {
 if(outputState == "off") {
 client.println("<p><button class=\"button\" onclick=\"outputOn()\">ON<
/button></p>");
 }
else {
 client.println("<p><button class=\"button
button2\"onclick=\"outputOff()\">OFF</button></p>");
}
}

Когда вы кликаете на кнопки «ON» и «OFF», за дело берется код ниже. В нем два оператора else if() включают или выключают выходной контакт, к которому подключено реле.

else if(header.indexOf("GET /?state=on") >= 0) {
 outputOn();
}
else if(header.indexOf("GET /?state=off") >= 0) {
outputOff();
}

Auto PIR (Режим PIR-датчика движения)

Теперь выберите при помощи выпадающего меню режим Auto PIR (Режим PIR-датчика).

На веб-странице появилось новое поле – «Timer (0 and 255 in seconds)». В него можно вписать целочисленное значение между «0» и «255», и это количество секунд, в течение которых реле должно оставаться включенным после обнаружения движения.

Когда вы меняете значение в этом поле, это выполняет следующую часть кода и меняет значение в переменной «timer».

else if(header.indexOf("GET /?timer=") >= 0) {
  pos1 = header.indexOf('=');
  pos2 = header.indexOf('&');
  valueString = header.substring(pos1+1, pos2);
  timer = valueString.toInt();
  EEPROM.write(2, timer);
  EEPROM.commit();
  Serial.println(valueString);
}

В этом режиме (1) на веб-странице будет показано только поле для таймера.

else if(selectedMode == 1) {
 client.println("<p>Timer (0 and 255 in seconds): <input type=\"number\"
name=\"txt\" value=\"" + String(EEPROM.read(2)) + "\"
onchange=\"setTimer(this.value)\" min=\"0\" max=\"255\"></p>");
}

Auto LDR (Режим фоторезистора)

Выберите режим фоторезистора. На веб-странице должно появиться новое поле «LDR Threshold».

В этом поле задается пороговое значение яркости для фоторезистора – от «0» до «100» (в процентах). Когда вы меняете значение в этом поле, вызывается фрагмент кода ниже (он обновляет пороговое значение для фоторезистора).

// задаем пороговое значение для фоторезистора:
else if(header.indexOf("GET /?ldrthreshold=") >= 0) {
  pos1 = header.indexOf('=');
  pos2 = header.indexOf('&');
  valueString = header.substring(pos1+1, pos2);
  ldrThreshold = valueString.toInt();
  EEPROM.write(3, ldrThreshold);
  EEPROM.commit();
  Serial.println(valueString);
}

Это режим 2, поэтому нам нужно показать поле для ввода порогового значения фоторезистора.

else if(selectedMode == 2) {
  client.println("<p>LDR Threshold (0 and 100%): <input type=\"number\"
name=\"txt\" value=\"" + String(EEPROM.read(3)) + "\"
onchange=\"setThreshold(this.value)\" min=\"0\" max=\"100\"></p>");
}

Auto PIR and LDR (Режим PIR-датчика и фоторезистора)

Если выбрать этот режим, в системе активируются и PIR-датчик движения, и фоторезистор. Кроме того, в браузере загрузится новая веб-страница с двумя полями для ввода данных – «Timer» и «LDR Threshold».

Оба этих поля работают так же, как и в режимах Auto PIR и Auto LDR.

Данные от датчика температуры и влажности

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

// считываем и показываем данные от датчика DHT:
if(header.indexOf("GET /?sensor") >= 0) {
  // данные от датчика 
  // могут приходить с 2-секундной задержкой
  // (это очень медленный датчик):
  float h = dht.readHumidity();
  // считываем температуру
  // в градусах Цельсия (по умолчанию):
  float t = dht.readTemperature();
  // считываем температуру
  // в градусах Фаренгейта (если «isFahrenheit» = «true»):
  float f = dht.readTemperature(true);
  // проверяем, удалось ли прочесть данные с датчика,
  // и если не удалось прочесть хоть какие-то из них,
  // выходим, чтобы попробовать снова:
  if (isnan (h) || isnan(t) || isnan(f)) {
    Serial.println("Failed to read from DHT sensor!");
                // "Не удалось прочесть
                //  данные от датчика DHT!"
    strcpy(celsiusTemp,"Failed");
                   //  "Данные прочесть не удалось"
    strcpy(fahrenheitTemp, "Failed");
                   //  "Данные прочесть не удалось"
    strcpy(humidityTemp, "Failed");  
                   //  "Данные прочесть не удалось"     
    }
  else {
    // рассчитываем температуру 
    // (в Цельсиях и Фаренгейтах) и влажность:
    float hic = dht.computeHeatIndex(t, h, false);       
    dtostrf(hic, 6, 2, celsiusTemp);             
    float hif = dht.computeHeatIndex(f, h);
    dtostrf(hif, 6, 2, fahrenheitTemp);         
    dtostrf(h, 6, 2, humidityTemp);
  }
  client.println("<p>");
  client.println(celsiusTemp);
  client.println("*C</p><p>");
  client.println(fahrenheitTemp);
  client.println("*F</p></div><p>");
  client.println(humidityTemp);
  client.println("%</p></div>");

Также показываем на веб-странице кнопку, с помощью которой эти данные можно спрятать.

client.println("<p><a href=\"/\"><button>Remove Sensor Readings</button></a></p>");

Итак, эти фрагменты кода отвечают за установку режима и его настроек. После этого запускается другая часть блока loop(), которая, исходя из заданных режима и настроек, проверяет, что нужно сделать с устройством вывода данных (реле) – включить или выключить.

Управление состоянием контакта для реле

К примеру, при обнаружении движения будет вызвана функция detectsMovement(), которая напишет в монитор порта сообщение «MOTION DETECTED!!!» («ОБНАРУЖЕНО ДВИЖЕНИЕ!») и запустит таймер.

void detectsMovement() {
  if(armMotion || (armMotion && armLdr)) {
    Serial.println("MOTION DETECTED!!!");
               //  "ОБНАРУЖЕНО ДВИЖЕНИЕ!!!"
    startTimer = true;
    lastMeasure = millis();
  }  
}

Затем (в зависимости от того, сколько прошло времени) реле либо включится, либо выключится.

// выбранный режим (1) – Режим PIR-датчика движения (Auto PIR):
  if(startTimer && armMotion && !armLdr) {
    if(outputState == "off") {
      outputOn();
    }
    else if((now - lastMeasure > (timer * 1000))) {
      outputOff();
      startTimer = false;     
    }
  }

Кроме того, фрагмент кода ниже включает/выключает контакт реле, исходя из того, упало ли значение от фоторезистора ниже заданного порога.

// выбранный режим (2) – Режим фоторезистора (Auto LDR):
// считываем текущее значение фоторезистора
// и соответствующим образом переключаем контакт реле:
if(armLdr && !armMotion) {
  int ldrValue = map(analogRead(ldr), 0, 4095, 0, 100); 
  //Serial.println(ldrValue);
  if(ldrValue > ldrThreshold && outputState == "on") {
    outputOff();
  }
  else if(ldrValue < ldrThreshold && outputState == "off") {
    outputOn();
  }
  delay(100);
}

Наконец, фрагмент кода ниже запускается, когда выбран режим 3 (совместный режим PIR-датчика движения и фоторезистора), и если PIR-датчик обнаружил движение, а значение фоторезистора упало ниже заданного порога.

// выбранный режим (3) –
// режим PIR-датчика движения и фоторезистора:
if(startTimer && armMotion && armLdr) {
  int ldrValue = map(analogRead(ldr), 0, 4095, 0, 100);
  //Serial.println(ldrValue);
  if(ldrValue > ldrThreshold) {
    outputOff();
    startTimer = false;
  }
  else if(ldrValue < ldrThreshold && outputState == "off") {
    outputOn();
  }
  else if(now - lastMeasure > (timer * 1000)) {
    outputOff();
    startTimer = false;
  }
}

Вот так и работает этот код. Кроме того, многие его фрагменты снабжены поясняющими комментариями – чтобы в нем было проще разобраться.

Проверяем WiFi-мультидатчик

Откройте монитор порта на скорости 115200 бод.

Нажмите на кнопку EN на ESP32, чтобы она напечатала в мониторе порта свой IP-адрес.

Затем откройте браузер и впишите этот IP-адрес в адресную строку. В результате должна загрузиться вот такая страница:

Теперь попробуйте выбрать с помощью выпадающего меню разные режимы и задать разные настройки, чтобы проверить, правильно ли все работает. К примеру, выберите «Ручной» режим (Manual), чтобы включить/выключить лампу.

Кликните на кнопку «View Sensor Readings» («Посмотреть данные от датчика»), чтобы в нижней части страницы появились самые последние данные от датчика температуры и влажности DHT.

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

Необходимое оборудование

Схема

На этой схеме изображена 36-контактная версия платы ESP32 DEVKIT DOIT V1. Если вы используете какую-то другую модель, обязательно сверьтесь с ее распиновкой.


Подключите эти компоненты друг к другу согласно схеме ниже. Подсоедините контакт данных PIR-датчика движения к контакту GPIO25, а фоторезистор – к GPIO33. Контакт данных датчика температуры и влажности DHT22 нужно подключить к GPIO27, а ножки RGB-светодиода – к контактам GPIO12, GPIO13 и GPIO14.

Кроме того, наш мультидатчик будет управлять устройством вывода данных – т.е. реле – подключенным к контакту GPIO2. С помощью этого реле мы будем управлять лампой, но вместо нее можно подключить и другое устройство.

Чтобы было понятнее, во время сборки цепи советуем сверяться с таблицами ниже.

Подключение PIR-датчика движения

PIR-датчик движения ESP32
VCC 3.3V (или VIN – в зависимости от модели вашего PIR-датчика)
Данные GPIO25
GND GND

Важно: PIR-датчик движения AM312, используемый в этом примере, работает на 3.3 вольтах. Но если вы, к примеру, используете PIR-датчик движения вроде HC-SR501, то он работает на 5 вольтах. И в этом случае вы можете либо модифицировать его, чтобы он работал на 3.3 вольтах, либо просто запитать его от контакта VIN.

Подключение RGB-светодиода

RGB-светодиод ESP32
Ножка для красного цвета GPIO14
Общий анод 3.3V
Ножка для зеленого цвета GPIO12
Ножка для синего цвета GPIO13

Подключение DHT22

DHT22 ESP32
Контакт 1 VIN (5 вольт)
Контакт 2 GPIO27 (через подтягивающий резистор на 10 кОм)
Контакт 3 Не нужно подключать
Контакт 4 GND

Подключение фоторезистора

Фоторезистор ESP32
Контакт 1 3.3V
Контакт 2 GPIO33 (через подтягивающий резистор на 10 кОм)

Подключение реле

Реле ESP32
VCC VIN (5V)
IN1 GPIO2
IN2 Не нужно подключать
GND GND
На этой схеме изображена 36-контактная версия платы ESP32 DEVKIT DOIT V1. Если вы используете какую-то другую модель, обязательно сверьтесь с ее распиновкой.


Создание корпуса

Этот корпус состоит из двух частей: дна и крышки. В крышке есть три круглых отверстия.

Одно из этих отверстий – для фоторезистора, второе – для PIR-датчика движения, а третье – для держателя RGB-светодиода. Кроме того, сбоку на крышке есть слот для датчика DHT, а также два прямоугольных выреза – для проводов реле и USB-кабеля (для питания ESP32).

ESP32 и реле-модуль необходимо расположить на дне. Кроме того, их можно понадежнее закрепить при помощи термоклея или клейкой ленты.

Наконец, у дна и крышки есть по 4 отверстия для 4 скрепляющих винтов.

Если у вас есть 3D-принтер, вы можете сделать такой корпус сами. Необходимые STL-файлыSketchUp-файл) для его печати можно скачать по этой ссылке.

Примечание: Советуем увеличить размер корпуса – так внутри него будет проще уместить все компоненты.

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

Далее проект нужно подготовить к установке в корпус – отключить компоненты от контактной макетной платы и подключить их друг к другу при помощи одной большой печатной макетной платы и двух маленьких кусочков, отрезанных от печатной макетной платы. Большая печатная макетная плата понадобится для подключения ESP32, а два кусочка – для подключения PIR-датчика движения и DHT-датчика. К дешевым компонентам вроде фоторезистора и RGB-светодиода провода можно припаять напрямую.

На фото ниже показано, как эта цепь будет выглядеть, будучи помещенной внутрь корпуса. Если вам не нравится паять, можете воспользоваться цепью, собранной на контактной макетной плате, но для этого варианта понадобится другой корпус.

Запитав ESP32 от USB-кабеля и подключив реле к лампе, закройте корпус и прикрепите крышку ко дну при помощи 4 шурупов.

Код

/*********
  Руи Сантос
  Более подробно о проекте на: http://randomnerdtutorials.com  
*********/

// загружаем библиотеки:
#include <WiFi.h>
#include <EEPROM.h>
#include "DHT.h"
#include <Adafruit_Sensor.h>

// вставьте в переменные ниже SSID и пароль для своей WiFi-сети:
const char* ssid     = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// три строчки ниже – для датчиков DHT11, DHT21 и DHT22;
// вам нужно убрать знаки комментария у строчки с датчиком,
// который вы используете, и две другие закомментировать:
//#define DHTTYPE DHT11   // DHT 11
//#define DHTTYPE DHT21   // DHT 21 (AM2301)
#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321

// задаем контакт, к которому подключен датчик DHT:
const int DHTPin = 27;
// инициализируем датчик DHT:
DHT dht(DHTPin, DHTTYPE);

// временные переменные для температуры и влажности:
static char celsiusTemp[7];
static char fahrenheitTemp[7];
static char humidityTemp[7];

// размер используемой flash-памяти;
// адрес 0: последнее состояние контакта для реле (0 = выкл, 1 = вкл)
// адрес 1: выбранный режим
// (0 = ручной режим, 1 = режим PIR-датчика, 2 = режим фоторезистора,
//  3 = режим PIR-датчика и фоторезистора)
// адрес 2: таймер (время между 0 и 255 секундами)
// адрес 3: пороговое значение для фоторезистора (между 0% и 100%)
#define EEPROM_SIZE 4

// задаем GPIO-контакты для
// устройства вывода данных (реле), RGB-светодиода, 
// PIR-датчика движения и фоторезистора:
const int output = 2;
const int redRGB = 14;
const int greenRGB = 12;
const int blueRGB = 13;
const int motionSensor = 25;
const int ldr = 33;
// в этой строке будет храниться текущее состояние контакта для реле:
String outputState = "off";

// вспомогательные переменные для таймера:
long now = millis();
long lastMeasure = 0;
boolean startTimer = false;

// вспомогательные переменные
// для хранения данных о выбранном режиме и настройках режимов:
int selectedMode = 0;
int timer = 0;
int ldrThreshold = 0;
int armMotion = 0;
int armLdr = 0;
String modes[4] = { "Manual", "Auto PIR", "Auto LDR", "Auto PIR and LDR" };

// переменные для декодирования HTTP-запроса GET:
String valueString = "0";
int pos1 = 0;
int pos2 = 0;
// переменная для хранения HTTP-запроса:
String header;
// создаем объект сервера и задаем ему номер порта «80»:
WiFiServer server(80);

void setup() {
  // инициализируем датчик DHT:
  dht.begin();

  // запускаем последовательную коммуникацию (в отладочных целях): 
  Serial.begin(115200);

  // задаем режим «INPUT_PULLUP» для контакта,
  // к которому подключен PIR-датчик движения,
  // а затем задаем для этого контакта прерывание
  // и выставляем у него режим «RISING»:
  pinMode(motionSensor, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);
  
  Serial.println("start...");  //  "запуск..."
  if(!EEPROM.begin(EEPROM_SIZE)) {
    Serial.println("failed to initialise EEPROM");
               //  "не удалось инициализировать EEPROM"
    delay(1000);
  }
  
  // раскомментируйте строчки ниже, чтобы проверить,
  // сохранились ли данные на flash-память:
  Serial.println(" bytes read from Flash . Values are:");
             //  " с flash-памяти считаны следующие данные:"
  for(int i = 0; i < EEPROM_SIZE; i++) {
    Serial.print(byte(EEPROM.read(i))); 
    Serial.print(" ");
  }
  
  // делаем контакты реле и RGB-светодиода выходными контактами: 
  pinMode(output, OUTPUT);
  pinMode(redRGB, OUTPUT);
  pinMode(greenRGB, OUTPUT);
  pinMode(blueRGB, OUTPUT);

  // считываем данные с flash-памяти,
  // а затем сохраняем их во вспомогательные переменные

  // присваиваем контакту, к которому подключено реле, 
  // самое последнее значение, хранящееся в flash-памяти:
  if(!EEPROM.read(0)) {
    outputState = "off";
    digitalWrite(output, HIGH);
  }
  else {
    outputState = "on";
    digitalWrite(output, LOW);
  }
  selectedMode = EEPROM.read(1);
  timer = EEPROM.read(2);
  ldrThreshold = EEPROM.read(3);
  configureMode();
  
  // подключаемся к WiFi-сети при помощи заданных выше SSID и пароля: 
  Serial.print("Connecting to ");  //  "Подключаемся к "
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // печатаем локальный IP-адрес и запускаем веб-сервер:
  Serial.println("");
  Serial.println("WiFi connected.");  //  "Подключились к WiFi."
  Serial.println("IP address: ");  //  "IP-адрес: "
  Serial.println(WiFi.localIP());
  server.begin();
}

void loop() {
  WiFiClient client = server.available();  // запускаем прослушку
                                           // входящих клиентов;
  if (client) {                            // при подключении
                                           // нового клиента
    Serial.println("New Client.");         // печатаем сообщение
                                           // об этом в монитор порта;
    String currentLine = "";               // делаем строку 
                                           // для хранения данных,
                                           // пришедших от клиента;                                           
    while (client.connected()) {           // пока клиент подключен,
      if (client.available()) {            // и если у клиента 
                                           // есть байты, которые
                                           // можно прочесть,
        char c = client.read();            // считываем байт
        Serial.write(c);                   // и печатаем его
                                           // в мониторе порта
        header += c;
        if (c == '\n') {                   // если считанный байт – 
                                           // это символ новой строки,
          // если вам попалось два символа новой строки подряд,
          // то это значит, что строка пуста;
          // это конец HTTP-запроса клиента, поэтому пора слать ответ: 
          if (currentLine.length() == 0) {
            // в начале HTTP-заголовка всегда пишется код ответа
            // (например, «HTTP/1.1 200 OK»), а также тип контента,
            // чтобы клиент всегда знал, что получает;
            // после этого пишем пустую линию:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
                       //  "Соединение: отключено"
            client.println();                     
            // показываем веб-страницу:
            client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            // используем CSS, чтобы задать стили
            // для кнопок «ON» и «OFF»;
            // не стесняйтесь экспериментировать с атрибутами
            // «background-color» и «font-size»,
            // чтобы дизайн этих кнопок  
            // максимально соответствовал вашим предпочтениям:
            client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;");
            client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println(".button2 {background-color: #555555;}</style></head>");
            
            // пример запроса: 
            // GET /?mode=0& HTTP/1.1 – он задает ручной режим (0):
            if(header.indexOf("GET /?mode=") >= 0) {
              pos1 = header.indexOf('=');
              pos2 = header.indexOf('&');
              valueString = header.substring(pos1+1, pos2);
              selectedMode = valueString.toInt();
              EEPROM.write(1, selectedMode);
              EEPROM.commit();
              configureMode();
            }
            // меняем значение GPIO-контакта,
            // к которому подключено реле,
            // включая и выключая его:
            else if(header.indexOf("GET /?state=on") >= 0) {
              outputOn();
            } 
            else if(header.indexOf("GET /?state=off") >= 0) {
              outputOff();
            }
            // задаем таймер:
            else if(header.indexOf("GET /?timer=") >= 0) {
              pos1 = header.indexOf('=');
              pos2 = header.indexOf('&');
              valueString = header.substring(pos1+1, pos2);
              timer = valueString.toInt();
              EEPROM.write(2, timer);
              EEPROM.commit();
              Serial.println(valueString);
            }
            // задаем пороговое значение для фоторезистора:
            else if(header.indexOf("GET /?ldrthreshold=") >= 0) {
              pos1 = header.indexOf('=');
              pos2 = header.indexOf('&');
              valueString = header.substring(pos1+1, pos2);
              ldrThreshold = valueString.toInt();
              EEPROM.write(3, ldrThreshold);
              EEPROM.commit();
              Serial.println(valueString);
            }
            
            // заголовок веб-страницы:
            client.println("<body><h1>ESP32 Web Server</h1>");
            // выпадающее меню для выбора режима:
            client.println("<p><strong>Mode selected:</strong> " + modes[selectedMode] + "</p>");
            client.println("<select id=\"mySelect\" onchange=\"setMode(this.value)\">");
            client.println("<option>Change mode");
            client.println("<option value=\"0\">Manual");
            client.println("<option value=\"1\">Auto PIR");
            client.println("<option value=\"2\">Auto LDR");
            client.println("<option value=\"3\">Auto PIR and LDR</select>");
          
            // показываем текущее состояние контакта для реле,
            // а также кнопки «ON» и «OFF» для управления им: 
            client.println("<p>GPIO - State " + outputState + "</p>");
            // если значение контакта для реле – это «выкл»,
            // на экране будет нарисована кнопка «ON»:       
            if(selectedMode == 0) {
              if(outputState == "off") {
                client.println("<p><button class=\"button\" onclick=\"outputOn()\">ON</button></p>");
              } 
              else {
                client.println("<p><button class=\"button button2\" onclick=\"outputOff()\">OFF</button></p>");
              }
            }
            else if(selectedMode == 1) {
              client.println("<p>Timer (0 and 255 in seconds): <input type=\"number\" name=\"txt\" value=\"" + 
                              String(EEPROM.read(2)) + "\" onchange=\"setTimer(this.value)\" min=\"0\" max=\"255\"></p>");
            }
            else if(selectedMode == 2) {
              client.println("<p>LDR Threshold (0 and 100%): <input type=\"number\" name=\"txt\" value=\"" + 
                              String(EEPROM.read(3)) + "\" onchange=\"setThreshold(this.value)\" min=\"0\" max=\"100\"></p>");
            }
            else if(selectedMode == 3) {
              client.println("<p>Timer (0 and 255 in seconds): <input type=\"number\" name=\"txt\" value=\"" + 
                               String(EEPROM.read(2)) + "\" onchange=\"setTimer(this.value)\" min=\"0\" max=\"255\"></p>");
              client.println("<p>LDR Threshold (0 and 100%): <input type=\"number\" name=\"txt\" value=\"" + 
                               String(EEPROM.read(3)) + "\" onchange=\"setThreshold(this.value)\" min=\"0\" max=\"100\"></p>");            
            }
            // считываем и показываем данные от датчика DHT:
            if(header.indexOf("GET /?sensor") >= 0) {
              // данные от датчика 
              // могут приходить с 2-секундной задержкой
              // (это очень медленный датчик):
              float h = dht.readHumidity();
              // считываем температуру
              // в градусах Цельсия (по умолчанию):
              float t = dht.readTemperature();
              // считываем температуру
              // в градусах Фаренгейта (если «isFahrenheit» = «true»):
              float f = dht.readTemperature(true);
              // проверяем, удалось ли прочесть данные с датчика,
              // и если не удалось прочесть хоть какие-то из них,
              // выходим, чтобы попробовать снова:
              if (isnan (h) || isnan(t) || isnan(f)) {
                Serial.println("Failed to read from DHT sensor!");
                            // "Не удалось прочесть
                            //  данные от датчика DHT!"
                strcpy(celsiusTemp,"Failed");
                               //  "Данные прочесть не удалось"
                strcpy(fahrenheitTemp, "Failed");
                               //  "Данные прочесть не удалось"
                strcpy(humidityTemp, "Failed");  
                               //  "Данные прочесть не удалось"     
              }
              else {
                // рассчитываем температуру 
                // (в Цельсиях и Фаренгейтах) и влажность:
                float hic = dht.computeHeatIndex(t, h, false);       
                dtostrf(hic, 6, 2, celsiusTemp);             
                float hif = dht.computeHeatIndex(f, h);
                dtostrf(hif, 6, 2, fahrenheitTemp);         
                dtostrf(h, 6, 2, humidityTemp);
                // функции Serial.print() ниже можно удалить
                // (они нужны лишь для отладочных целей):
                /*Serial.print("Humidity: "); Serial.print(h); Serial.print(" %\t Temperature: "); 
                Serial.print(t); Serial.print(" *C "); Serial.print(f); 
                Serial.print(" *F\t Heat index: "); Serial.print(hic); Serial.print(" *C "); 
                Serial.print(hif); Serial.print(" *F"); Serial.print("Humidity: "); 
                Serial.print(h); Serial.print(" %\t Temperature: "); Serial.print(t);
                Serial.print(" *C "); Serial.print(f); Serial.print(" *F\t Heat index: ");
                Serial.print(hic); Serial.print(" *C "); Serial.print(hif); Serial.println(" *F");*/
              }
              client.println("<p>");
              client.println(celsiusTemp);
              client.println("*C</p><p>");
              client.println(fahrenheitTemp);
              client.println("*F</p></div><p>");
              client.println(humidityTemp);
              client.println("%</p></div>");
              client.println("<p><a href=\"/\"><button>Remove Sensor Readings</button></a></p>");
            }
            else {
              client.println("<p><a href=\"?sensor\"><button>View Sensor Readings</button></a></p>");
            }
            client.println("<script> function setMode(value) { var xhr = new XMLHttpRequest();"); 
            client.println("xhr.open('GET', \"/?mode=\" + value + \"&\", true);"); 
            client.println("xhr.send(); location.reload(true); } ");
            client.println("function setTimer(value) { var xhr = new XMLHttpRequest();");
            client.println("xhr.open('GET', \"/?timer=\" + value + \"&\", true);"); 
            client.println("xhr.send(); location.reload(true); } ");
            client.println("function setThreshold(value) { var xhr = new XMLHttpRequest();");
            client.println("xhr.open('GET', \"/?ldrthreshold=\" + value + \"&\", true);"); 
            client.println("xhr.send(); location.reload(true); } ");
            client.println("function outputOn() { var xhr = new XMLHttpRequest();");
            client.println("xhr.open('GET', \"/?state=on\", true);"); 
            client.println("xhr.send(); location.reload(true); } ");
            client.println("function outputOff() { var xhr = new XMLHttpRequest();");
            client.println("xhr.open('GET', \"/?state=off\", true);"); 
            client.println("xhr.send(); location.reload(true); } ");
            client.println("function updateSensorReadings() { var xhr = new XMLHttpRequest();");
            client.println("xhr.open('GET', \"/?sensor\", true);"); 
            client.println("xhr.send(); location.reload(true); }</script></body></html>");
            // HTTP-ответ заканчивается еще одной пустой строкой:
            client.println();
            // выходим из цикла while():
            break;
          } else { // если получили символ новой строки,
                   // очищаем переменную «currentLine»:
            currentLine = "";
          }
        } else if (c != '\r') {  // если получили какие-нибудь данные
                                 // (кроме символа возврата каретки),
          currentLine += c;      // добавляем их в конец «currentLine» 
        }
      }
    }
    // очищаем переменную «header»:
    header = "";
    // отключаем соединение:
    client.stop();
    Serial.println("Client disconnected.");
               //  "Клиент отключился."
  }
  
  // запускаем таймер для включения/выключения контакта реле
  // согласно заданному времени и/или значению от фоторезистора:
  now = millis();
  
  // выбранный режим (1) – Режим PIR-датчика движения (Auto PIR):
  if(startTimer && armMotion && !armLdr) {
    if(outputState == "off") {
      outputOn();
    }
    else if((now - lastMeasure > (timer * 1000))) {
      outputOff();
      startTimer = false;     
    }
  }  
  
  // выбранный режим (2) – Режим фоторезистора (Auto LDR):
  // считываем текущее значение фоторезистора
  // и соответствующим образом переключаем контакт реле:
  if(armLdr && !armMotion) {
    int ldrValue = map(analogRead(ldr), 0, 4095, 0, 100); 
    //Serial.println(ldrValue);
    if(ldrValue > ldrThreshold && outputState == "on") {
      outputOff();
    }
    else if(ldrValue < ldrThreshold && outputState == "off") {
      outputOn();
    }
    delay(100);
  }
  
  // выбранный режим (3) –
  // режим PIR-датчика движения и фоторезистора:
  if(startTimer && armMotion && armLdr) {
    int ldrValue = map(analogRead(ldr), 0, 4095, 0, 100);
    //Serial.println(ldrValue);
    if(ldrValue > ldrThreshold) {
      outputOff();
      startTimer = false;
    }
    else if(ldrValue < ldrThreshold && outputState == "off") {
      outputOn();
    }
    else if(now - lastMeasure > (timer * 1000)) {
      outputOff();
      startTimer = false;
    }
  }
}

// проверяем, обнаружены ли движение (с помощью PIR-датчика)
// и затемнение (с помощью фоторезистора), а затем запускаем таймер:
void detectsMovement() {
  if(armMotion || (armMotion && armLdr)) {
    Serial.println("MOTION DETECTED!!!");
               //  "ОБНАРУЖЕНО ДВИЖЕНИЕ!!!"
    startTimer = true;
    lastMeasure = millis();
  }  
}
void configureMode() {
  // ручной режим:
  if(selectedMode == 0) {
    armMotion = 0;
    armLdr = 0;
    // цвет RGB-светодиода - красный:
    digitalWrite(redRGB, LOW);
    digitalWrite(greenRGB, HIGH);
    digitalWrite(blueRGB, HIGH);
  }
  // режим PIR-датчика движения:
  else if(selectedMode == 1) {
    outputOff();
    armMotion = 1;
    armLdr = 0;
    // цвет RGB-светодиода – зеленый:
    digitalWrite(redRGB, HIGH);
    digitalWrite(greenRGB, LOW);
    digitalWrite(blueRGB, HIGH);
  }
  // режим фоторезистора:
  else if(selectedMode == 2) {
    armMotion = 0;
    armLdr = 1;
    // цвет RGB-светодиода – синий:    
    digitalWrite(redRGB, HIGH);
    digitalWrite(greenRGB, HIGH);
    digitalWrite(blueRGB, LOW);
  }
  // режим PIR-датчика движения и фоторезистора:
  else if(selectedMode == 3) {
    outputOff();
    armMotion = 1;
    armLdr = 1;
    // цвет RGB-светодиода – фиолетовый:    
    digitalWrite(redRGB, LOW);
    digitalWrite(greenRGB, HIGH);
    digitalWrite(blueRGB, LOW);
  }
}	

// переключаем контакт реле с «on» («вкл») на «off» («выкл»):
void outputOn() {
  Serial.println("GPIO on");
  outputState = "on";
  digitalWrite(output, LOW);
  EEPROM.write(0, 1);
  EEPROM.commit();
}
void outputOff() { 
  Serial.println("GPIO off");
  outputState = "off";
  digitalWrite(output, HIGH);
  EEPROM.write(0, 0);
  EEPROM.commit();
}

См.также

Внешние ссылки