ESP32 IoT Shield: печатная плата с панелью управления для выходов и датчиков
В этом проекте мы покажем вам, как создать IoT-плату расширения (shield PCB) для ESP32 и веб-панель управления для её контроля. Плата оснащена датчиком BME280 (температура, влажность и давление), фоторезистором LDR (датчик освещённости), PIR-датчиком движения, статусным светодиодом, кнопкой и клеммным разъёмом для подключения модуля реле или любого другого устройства вывода.
Также вы можете повторить этот проект, собрав схему на макетной плате.
Смотрите видеоурок
Этот проект доступен в видеоформате и в текстовом формате. Вы можете посмотреть видео ниже или прокрутить страницу для текстовых инструкций.
Ресурсы
Все необходимые ресурсы для сборки этого проекта доступны по ссылкам ниже (или вы можете посетить проект на GitHub):
`Нажмите здесь, чтобы скачать все файлы <https://github.com/RuiSantosdotme/ESP32-IoT-Shield-PCB/archive/master.zip>`_
Обзор проекта
Этот проект состоит из двух частей:
Проектирование и сборка IoT-платы расширения
Программирование IoT-платы с помощью Arduino IDE
Возможности IoT Shield
IoT-плата расширения разработана для установки поверх ESP32. По этой причине, если вы хотите использовать нашу печатную плату, вам нужна та же модель ESP32. Мы используем плату ESP32 DEVKIT DOIT V1 (версия с 36 GPIO).
Если у вас другая модель ESP32, вы всё равно можете повторить этот проект, собрав схему на макетной плате или изменив компоновку печатной платы и разводку под вашу плату ESP32.
Плата расширения включает:
Датчик температуры, влажности и давления BME280;
Фоторезистор LDR (датчик освещённости);
PIR-датчик движения;
Статусный светодиод на плате;
Кнопку;
3-контактный разъём с доступом к GND, 5V и GPIO, к которому можно подключить любое устройство вывода (например, модуль реле).
Назначение выводов ESP32 IoT Shield
В следующей таблице описано назначение выводов для каждого компонента IoT-платы:
Компонент |
Вывод ESP32 |
|---|---|
BME280 |
GPIO 21 (SDA), GPIO 22 (SCL) |
PIR-датчик движения |
GPIO 27 |
Фоторезистор (LDR) |
GPIO 33 |
Кнопка |
GPIO 18 |
Светодиод |
GPIO 19 |
Дополнительный выход |
GPIO 32 |
Если вы хотите назначить и использовать другие выводы, ознакомьтесь с нашим руководством по выводам ESP32.
Возможности веб-сервера (IoT Dashboard)
Для управления платой расширения мы создадим веб-сервер. Однако вы можете запрограммировать плату по-своему, с другим веб-сервером или интегрировать её с платформой домашней автоматизации.
Вот функции веб-сервера для управления IoT-платой:
Для доступа к веб-серверу необходимо ввести логин и пароль (см.: ESP32 Web Server HTTP Authentication).
После аутентификации с правильными учётными данными вы получаете доступ к веб-серверу. В верхней части страницы есть иконка для выхода из системы. После нажатия вам потребуется повторная авторизация.
Два переключателя: один для управления выходным разъёмом, другой для статусного светодиода на плате.
Статусный светодиод также можно переключать физической кнопкой на плате. Состояние светодиода автоматически обновляется на веб-странице (как в этом уроке: Управление выходами с веб-сервера и физической кнопкой одновременно). Переключатель статусного светодиода может быть полезен для активации или деактивации чего-либо на ESP32, а светодиод даёт визуальную обратную связь.
Температура, влажность и освещённость отображаются на веб-сервере и автоматически обновляются с помощью серверных событий (SSE).
Наконец, есть карточка, которая показывает, обнаружено ли движение. После получения уведомления «Движение обнаружено» вы можете нажать на карточку, чтобы сбросить предупреждение.
Это основные функции IoT-панели ESP32, которую мы будем создавать. Она объединяет многие темы, рассмотренные в предыдущих уроках.
Это лишь пример того, как можно управлять платой расширения. Идея в том, чтобы модифицировать код и добавить свои собственные функции в проект.
Тестирование схемы на макетной плате
Перед проектированием и изготовлением печатной платы важно протестировать схему на макетной плате. Если вы не хотите делать печатную плату, вы всё равно можете повторить этот проект, собрав схему на макетной плате.
Необходимые компоненты
Для сборки схемы на макетной плате вам понадобятся следующие компоненты:
DOIT ESP32 DEVKIT V1 (версия с 36 GPIO) – читайте Лучшие платы разработки ESP32
1x BME280 (4 вывода)
Собрав все компоненты, соберите схему по следующей принципиальной схеме:
Проектирование печатной платы
Для проектирования схемы и печатной платы мы использовали EasyEDA, браузерную программу для разработки печатных плат. Если вы хотите модифицировать свою печатную плату, вам просто нужно загрузить следующие файлы:
Проектирование схемы работает так же, как в любом другом инструменте для проектирования схем: вы размещаете компоненты и соединяете их. Затем каждому компоненту назначается посадочное место (footprint).
Назначив компоненты, разместите каждый из них. Когда вы будете довольны компоновкой, выполните все соединения и трассировку печатной платы.
Сохраните проект и экспортируйте Gerber-файлы.
Примечание: вы можете скачать файлы проекта и отредактировать их для своих нужд.
`Скачать Gerber .zip файл <https://github.com/RuiSantosdotme/ESP32-IoT-Shield-PCB/raw/master/Gerber_PCB_ESP32_IoT_Shield_2020-06-10_10-55-31.zip>`_
Заказ печатных плат на PCBWay
Этот проект спонсируется PCBWay. PCBWay – это полнофункциональный сервис производства печатных плат.
Превратите ваши самодельные схемы на макетной плате в профессиональные печатные платы – получите 10 плат примерно за $5 + доставка (которая будет зависеть от вашей страны).
Когда у вас есть Gerber-файлы, вы можете заказать печатную плату. Выполните следующие шаги для загрузки файла.
Скачайте Gerber-файлы – нажмите здесь, чтобы скачать .zip файл
Перейдите на сайт PCBWay и откройте страницу быстрого расчёта PCB.
PCBWay может автоматически считать все параметры PCB и заполнить их за вас. Используйте «Quick-order PCB (Autofill parameters)».
Нажмите кнопку «+ Add Gerber file», чтобы загрузить предоставленные Gerber-файлы.
Вот и всё. Вы также можете использовать OnlineGerberViewer, чтобы проверить, правильно ли выглядит ваша печатная плата.
Если вы не торопитесь, можно использовать способ доставки China Post, чтобы значительно снизить стоимость. По нашему мнению, они завышают время доставки China Post.
Вы можете увеличить количество печатных плат и изменить цвет маски. Я заказал синий цвет.
Когда вы будете готовы, можете заказать платы, нажав «Save to Cart» и завершив заказ.
Распаковка
Примерно через неделю при доставке DHL я получил печатные платы в свой офис.
Всё хорошо упаковано, и печатные платы действительно высокого качества. Надписи на шелкографии чётко напечатаны и легко читаются. Кроме того, припой легко пристаёт к контактным площадкам.
Помимо печатных плат, я также получил несколько наклеек, линейку и ручку. В целом мы очень довольны сервисом PCBWay.
Пайка компонентов
Следующий шаг – пайка компонентов на печатную плату. Я использовал SMD-светодиод и SMD-резисторы. Их может быть немного сложно паять, но они экономят много места на печатной плате.
Вот список всех компонентов, необходимых для сборки платы расширения:
1x SMD-светодиод (1206)
1x SMD-резистор 330 Ом (1206)
2x SMD-резистора 10 кОм (1206)
1x кнопка (0.55 мм)
Гнездовые штыревые разъёмы (2.54 мм)
Вот инструменты для пайки, которые я использовал:
Читайте наш обзор паяльника TS80: TS80 Soldering Iron Review – Best Portable Soldering Iron.
Начните с пайки SMD-компонентов. Затем припаяйте штыревые разъёмы. И наконец, припаяйте остальные компоненты или используйте штыревые разъёмы, если не хотите подключать компоненты постоянно.
Вот как выглядит IoT Shield для ESP32 после сборки всех компонентов. Она должна идеально подключаться к плате ESP32 DEVKIT DOIT V1.
Программирование ESP32 IoT Shield
Код для этого проекта запускает веб-сервер, который позволяет вам мониторить и управлять IoT-платой. Функции веб-сервера были описаны выше.
Мы будем программировать плату ESP32 с помощью Arduino IDE. Убедитесь, что у вас установлено дополнение для плат ESP32.
Установка библиотек
Перед загрузкой кода убедитесь, что у вас установлены следующие библиотеки:
ESPAsyncWebServer от ESP32Async
AsyncTCP от ESP32Async
Для установки этих библиотек в Arduino IDE перейдите в Sketch > Include Library > Manage Libraries. Откроется Library Manager. Найдите библиотеки по имени и установите их.
Код – веб-панель ESP32 IoT Shield
Скопируйте следующий код в Arduino IDE.
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-iot-shield-pcb-dashboard/
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*********/
// Import required libraries
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Web Server HTTP Authentication credentials
const char* http_username = "admin";
const char* http_password = "admin";
Adafruit_BME280 bme; // BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)
const int buttonPin = 18; // Pushbutton
const int ledPin = 19; // Status LED
const int output = 32; // Output socket
const int ldr = 33; // LDR (Light Dependent Resistor)
const int motionSensor = 27; // PIR Motion Sensor
int ledState = LOW; // current state of the output pin
int buttonState; // current reading from the input pin
int lastButtonState = LOW; // previous reading from the input pin
bool motionDetected = false; // flag variable to send motion alert message
bool clearMotionAlert = true; // clear last motion alert message from web page
unsigned long lastDebounceTime = 0; // the last time the output pin was toggled
unsigned long debounceDelay = 50; // the debounce time; increase if the output flickers
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncEventSource events("/events");
const char* PARAM_INPUT_1 = "state";
// Checks if motion was detected
void IRAM_ATTR detectsMovement() {
//Serial.println("MOTION DETECTED!!!");
motionDetected = true;
clearMotionAlert = false;
}
// Main HTML web page in root url /
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>ESP IOT DASHBOARD</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<style>
html {font-family: Arial; display: inline-block; text-align: center;}
h3 {font-size: 1.8rem; color: white;}
h4 { font-size: 1.2rem;}
p { font-size: 1.4rem;}
body { margin: 0;}
.switch {position: relative; display: inline-block; width: 120px; height: 68px; margin-bottom: 20px;}
.switch input {display: none;}
.slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 68px; opacity: 0.8; cursor: pointer;}
.slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 68px}
input:checked+.slider {background-color: #1b78e2}
input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
.topnav { overflow: hidden; background-color: #1b78e2;}
.content { padding: 20px;}
.card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);}
.cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));}
.slider2 { -webkit-appearance: none; margin: 14px; height: 20px; background: #ccc; outline: none; opacity: 0.8; -webkit-transition: .2s; transition: opacity .2s; margin-bottom: 40px; }
.slider:hover, .slider2:hover { opacity: 1; }
.slider2::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 40px; height: 40px; background: #008B74; cursor: pointer; }
.slider2::-moz-range-thumb { width: 40px; height: 40px; background: #008B74; cursor: pointer;}
.reading { font-size: 2.6rem;}
.card-switch {color: #50a2ff; }
.card-light{ color: #008B74;}
.card-bme{ color: #572dfb;}
.card-motion{ color: #3b3b3b; cursor: pointer;}
.icon-pointer{ cursor: pointer;}
</style>
</head>
<body>
<div class="topnav">
<h3>ESP IOT DASHBOARD <span style="text-align:right;"> <i class="fas fa-user-slash icon-pointer" onclick="logoutButton()"></i></span></h3>
</div>
<div class="content">
<div class="cards">
%BUTTONPLACEHOLDER%
<div class="card card-bme">
<h4><i class="fas fa-chart-bar"></i> TEMPERATURE</h4><div><p class="reading"><span id="temp"></span>°C</p></div>
</div>
<div class="card card-bme">
<h4><i class="fas fa-chart-bar"></i> HUMIDITY</h4><div><p class="reading"><span id="humi"></span>%</p></div>
</div>
<div class="card card-light">
<h4><i class="fas fa-sun"></i> LIGHT</h4><div><p class="reading"><span id="light"></span></p></div>
</div>
<div class="card card-motion" onClick="clearMotionAlert()">
<h4><i class="fas fa-running"></i> MOTION SENSOR</h4><div><p class="reading"><span id="motion">%MOTIONMESSAGE%</span></p></div>
</div>
</div>
<script>
function logoutButton() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/logout", true);
xhr.send();
setTimeout(function(){ window.open("/logged-out","_self"); }, 1000);
}
function controlOutput(element) {
var xhr = new XMLHttpRequest();
if(element.checked){ xhr.open("GET", "/output?state=1", true); }
else { xhr.open("GET", "/output?state=0", true); }
xhr.send();
}
function toggleLed(element) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/toggle", true);
xhr.send();
}
function clearMotionAlert() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/clear-motion", true);
xhr.send();
setTimeout(function(){
document.getElementById("motion").textContent = "No motion";
document.getElementById("motion").style.color = "#3b3b3b";
}, 1000);
}
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('message', function(e) {
console.log("message", e.data);
}, false);
source.addEventListener('led_state', function(e) {
console.log("led_state", e.data);
var inputChecked;
if( e.data == 1){ inputChecked = true; }
else { inputChecked = false; }
document.getElementById("led").checked = inputChecked;
}, false);
source.addEventListener('motion', function(e) {
console.log("motion", e.data);
document.getElementById("motion").textContent = e.data;
document.getElementById("motion").style.color = "#b30000";
}, false);
source.addEventListener('temperature', function(e) {
console.log("temperature", e.data);
document.getElementById("temp").textContent = e.data;
}, false);
source.addEventListener('humidity', function(e) {
console.log("humidity", e.data);
document.getElementById("humi").textContent = e.data;
}, false);
source.addEventListener('light', function(e) {
console.log("light", e.data);
document.getElementById("light").textContent = e.data;
}, false);
}</script>
</body>
</html>)rawliteral";
String outputState(int gpio){
if(digitalRead(gpio)){
return "checked";
}
else {
return "";
}
}
String processor(const String& var){
//Serial.println(var);
if(var == "BUTTONPLACEHOLDER"){
String buttons;
String outputStateValue = outputState(32);
buttons+="<div class=\"card card-switch\"><h4><i class=\"fas fa-lightbulb\"></i> OUTPUT</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"controlOutput(this)\" id=\"output\" " + outputStateValue + "><span class=\"slider\"></span></label></div>";
outputStateValue = outputState(19);
buttons+="<div class=\"card card-switch\"><h4><i class=\"fas fa-lightbulb\"></i> STATUS LED</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleLed(this)\" id=\"led\" " + outputStateValue + "><span class=\"slider\"></span></label></div>";
return buttons;
}
else if(var == "MOTIONMESSAGE"){
if(!clearMotionAlert) {
return String("<span style=\"color:#b30000;\">MOTION DETECTED!</span>");
}
else {
return String("No motion");
}
}
return String();
}
// Logged out web page
const char logout_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>Logged out or <a href="/">return to homepage</a>.</p>
<p><strong>Note:</strong> close all web browser tabs to complete the logout process.</p>
</body>
</html>
)rawliteral";
void setup(){
// Serial port for debugging purposes
Serial.begin(115200);
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
// initialize the pushbutton pin as an input
pinMode(buttonPin, INPUT);
// initialize the LED pin as an output
pinMode(ledPin, OUTPUT);
// initialize the LED pin as an output
pinMode(output, OUTPUT);
// PIR Motion Sensor mode INPUT_PULLUP
pinMode(motionSensor, INPUT_PULLUP);
// Set motionSensor pin as interrupt, assign interrupt function and set RISING mode
attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
// Print ESP32 Local IP Address
Serial.println(WiFi.localIP());
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
if(!request->authenticate(http_username, http_password))
return request->requestAuthentication();
request->send_P(200, "text/html", index_html, processor);
});
server.on("/logged-out", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", logout_html, processor);
});
server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(401);
});
// Send a GET request to control output socket <ESP_IP>/output?state=<inputMessage>
server.on("/output", HTTP_GET, [] (AsyncWebServerRequest *request) {
if(!request->authenticate(http_username, http_password))
return request->requestAuthentication();
String inputMessage;
// GET gpio and state value
if (request->hasParam(PARAM_INPUT_1)) {
inputMessage = request->getParam(PARAM_INPUT_1)->value();
digitalWrite(output, inputMessage.toInt());
request->send(200, "text/plain", "OK");
}
request->send(200, "text/plain", "Failed");
});
// Send a GET request to control on board status LED <ESP_IP>/toggle
server.on("/toggle", HTTP_GET, [] (AsyncWebServerRequest *request) {
if(!request->authenticate(http_username, http_password))
return request->requestAuthentication();
ledState = !ledState;
digitalWrite(ledPin, ledState);
request->send(200, "text/plain", "OK");
});
// Send a GET request to clear the "Motion Detected" message <ESP_IP>/clear-motion
server.on("/clear-motion", HTTP_GET, [] (AsyncWebServerRequest *request) {
if(!request->authenticate(http_username, http_password))
return request->requestAuthentication();
clearMotionAlert = true;
request->send(200, "text/plain", "OK");
});
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
// send event with message "hello!", id current millis and set reconnect delay to 1 second
client->send("hello!",NULL,millis(),1000);
});
server.addHandler(&events);
// Start server
server.begin();
}
void loop(){
static unsigned long lastEventTime = millis();
static const unsigned long EVENT_INTERVAL_MS = 10000;
// read the state of the switch into a local variable
int reading = digitalRead(buttonPin);
// If the switch changed
if (reading != lastButtonState) {
// reset the debouncing timer
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
// if the button state has changed:
if (reading != buttonState) {
buttonState = reading;
// only toggle the LED if the new button state is HIGH
if (buttonState == HIGH) {
ledState = !ledState;
digitalWrite(ledPin, ledState);
events.send(String(digitalRead(ledPin)).c_str(),"led_state",millis());
}
}
}
if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
events.send("ping",NULL,millis());
events.send(String(bme.readTemperature()).c_str(),"temperature",millis());
events.send(String(bme.readHumidity()).c_str(),"humidity",millis());
events.send(String(analogRead(ldr)).c_str(),"light",millis());
lastEventTime = millis();
}
if(motionDetected & !clearMotionAlert){
events.send(String("MOTION DETECTED!").c_str(),"motion",millis());
motionDetected = false;
}
// save the reading. Next time through the loop, it'll be the lastButtonState:
lastButtonState = reading;
}
Этот код довольно длинный для объяснения, поэтому вы можете просто заменить следующие две переменные своими учётными данными сети, и код будет работать сразу.
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Если вы хотите узнать, как работает этот код, продолжайте чтение. В противном случае вы можете перейти к разделу Демонстрация.
Как работает код
Прочитайте этот раздел, если хотите узнать, как работает код, или перейдите к следующему разделу.
Следующие строки импортируют необходимые библиотеки:
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>
Вставьте ваши учётные данные сети в следующие строки, чтобы ESP32 мог подключиться к вашей сети.
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Следующие строки определяют имя пользователя и пароль для доступа к веб-серверу. По умолчанию логин – admin, пароль – admin. Вы можете изменить их в следующих строках:
// Web Server HTTP Authentication credentials
const char* http_username = "admin";
const char* http_password = "admin";
Создание объекта Adafruit_BME280 с именем bme. Это создаёт I2C-соединение с BME280 на GPIO 21 и GPIO 22.
Adafruit_BME280 bme; // BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)
Затем определите GPIO, к которым подключены компоненты платы расширения.
const int buttonPin = 18; // Pushbutton
const int ledPin = 19; // Status LED
const int output = 32; // Output socket
const int ldr = 33; // LDR (Light Dependent Resistor)
const int motionSensor = 27; // PIR Motion Sensor
Создайте следующие переменные для хранения состояний. Комментарии объясняют, что означает каждая переменная.
int ledState = LOW; // current state of the output pin
int buttonState; // current reading from the input pin
int lastButtonState = LOW; // previous reading from the input pin
bool motionDetected = false; // flag variable to send motion alert message
bool clearMotionAlert = true; // clear last motion alert message from web page
Переменные lastDebounceTime и debounceDelay используются для устранения дребезга кнопки. Это предотвращает ложные срабатывания.
unsigned long lastDebounceTime = 0; // the last time the output pin was toggled
unsigned long debounceDelay = 50; // the debounce time; increase if the output flickers
Создание объекта AsyncWebServer на порту 80.
AsyncWebServer server(80);
Для автоматического отображения информации на веб-сервере при появлении новых показаний мы используем серверные события (Server-Sent Events, SSE).
Следующая строка создаёт новый источник событий по адресу /events. Серверные события позволяют веб-странице (клиенту) получать обновления от сервера.
AsyncEventSource events("/events");
Переменная PARAM_INPUT_1 будет использоваться для проверки, содержит ли определённый URL-запрос параметр «state».
const char* PARAM_INPUT_1 = "state";
Функция обратного вызова прерывания
Функция обратного вызова detectsMovement() будет вызвана, когда PIR-датчик движения обнаружит движение (срабатывает прерывание). Функция изменяет состояние переменной motionDetected на true, чтобы мы знали, что движение обнаружено, и устанавливает переменную clearMotionAlert в false, потому что мы хотим, чтобы сообщение «Движение обнаружено» отображалось на веб-сервере.
void IRAM_ATTR detectsMovement() {
//Serial.println("MOTION DETECTED!!!");
motionDetected = true;
clearMotionAlert = false;
}
Построение веб-страницы
Переменная index_html содержит весь HTML, CSS и JavaScript для создания веб-страницы. Мы не будем вдаваться в подробности работы HTML и CSS. Мы рассмотрим только обработку событий, отправляемых сервером.
Обработка событий
Создайте новый объект EventSource и укажите URL страницы, отправляющей обновления. В нашем случае это /events.
if (!!window.EventSource) {
var source = new EventSource('/events');
После создания экземпляра источника событий вы можете начать прослушивание сообщений от сервера с помощью addEventListener().
Это стандартные обработчики событий, как показано в документации AsyncWebServer.
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('message', function(e) {
console.log("message", e.data);
}, false);
Затем добавьте остальные обработчики событий.
Когда вы изменяете состояние статусного светодиода, ESP32 отправляет событие (led_state) с этой информацией, чтобы панель управления обновлялась автоматически.
source.addEventListener('led_state', function(e) {
console.log("led_state", e.data);
var inputChecked;
if( e.data == 1){ inputChecked = true; }
else { inputChecked = false; }
document.getElementById("led").checked = inputChecked;
}, false);
Когда браузер получает это событие, он изменяет состояние элемента переключателя.
Событие motion отправляется при обнаружении движения. Когда это происходит, содержимое сообщения изменяется, а также меняется его цвет.
source.addEventListener('motion', function(e) {
console.log("motion", e.data);
document.getElementById("motion").textContent = e.data;
document.getElementById("motion").style.color = "#b30000";
}, false);
События temperature, humidity и light отправляются в браузер при появлении новых показаний.
source.addEventListener('temperature', function(e) {
console.log("temperature", e.data);
document.getElementById("temp").textContent = e.data;
}, false);
source.addEventListener('humidity', function(e) {
console.log("humidity", e.data);
document.getElementById("humi").textContent = e.data;
}, false);
source.addEventListener('light', function(e) {
console.log("light", e.data);
document.getElementById("light").textContent = e.data;
}, false);
Когда это происходит, мы помещаем полученные данные в элементы с соответствующим id.
Функция outputState()
Функция outputState() используется для проверки текущего состояния вывода GPIO. Она возвращает «checked», если GPIO включён, или пустую строку, если нет. Возвращаемая строка будет использоваться для построения веб-страницы с текущими состояниями выходов. Таким образом, каждый раз при доступе к веб-серверу вы видите текущие состояния.
String outputState(int gpio){
if(digitalRead(gpio)){
return "checked";
}
else {
return "";
}
}
Функция processor()
Функция processor() заменяет заполнители в HTML-тексте любой нужной строкой. Мы используем функцию processor(), чтобы при первом доступе к веб-серверу в новой вкладке браузера отображались текущие состояния GPIO и датчика движения.
Заполнитель BUTTONPLACEHOLDER заменяется HTML-текстом для создания кнопки с правильными состояниями.
if(var == "BUTTONPLACEHOLDER"){
String buttons;
String outputStateValue = outputState(32);
buttons+="<div class=\"card card-switch\"><h4><i class=\"fas fa-lightbulb\"></i> OUTPUT</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"controlOutput(this)\" id=\"output\" " + outputStateValue + "><span class=\"slider\"></span></label></div>";
outputStateValue = outputState(19);
buttons+="<div class=\"card card-switch\"><h4><i class=\"fas fa-lightbulb\"></i> STATUS LED</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleLed(this)\" id=\"led\" " + outputStateValue + "><span class=\"slider\"></span></label></div>";
return buttons;
}
Заполнитель MOTIONMESSAGE заменяется сообщением MOTION DETECTED или No motion, в зависимости от текущего состояния датчика движения.
else if(var == "MOTIONMESSAGE"){
if(!clearMotionAlert) {
return String("<span style=\"color:#b30000;\">MOTION DETECTED!</span>");
}
else {
return String("No motion");
}
}
return String();
Страница выхода из системы
Переменная logout_html содержит HTML-текст для построения страницы выхода. Вы перенаправляетесь на страницу выхода при нажатии кнопки logout на веб-странице.
const char logout_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>Logged out or <a href="/">return to homepage</a>.</p>
<p><strong>Note:</strong> close all web browser tabs to complete the logout process.</p>
</body>
</html>
)rawliteral";
На странице выхода есть ссылка, позволяющая вернуться на страницу входа (корневой URL /).
<p>Logged out or <a href="/">return to homepage</a>.</p>
Функция setup()
В setup() инициализируйте монитор последовательного порта.
Serial.begin(115200);
Инициализируйте датчик BME280.
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
Установите кнопку как вход, статусный светодиод и дополнительный выход как выходы, а PIR-датчик движения как прерывание.
// initialize the pushbutton pin as an input
pinMode(buttonPin, INPUT);
// initialize the LED pin as an output
pinMode(ledPin, OUTPUT);
// initialize the LED pin as an output
pinMode(output, OUTPUT);
// PIR Motion Sensor mode INPUT_PULLUP
pinMode(motionSensor, INPUT_PULLUP);
// Set motionSensor pin as interrupt, assign interrupt function and set RISING mode
attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);
Подключитесь к Wi-Fi и выведите IP-адрес ESP32.
// Connect to Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
// Print ESP32 Local IP Address
Serial.println(WiFi.localIP());
Обработка запросов
Нам нужно обработать, что происходит, когда ESP32 получает запрос по определённому URL.
Обработка запросов с аутентификацией
Каждый раз, когда вы отправляете запрос к ESP32 для доступа к веб-серверу, он проверяет, ввели ли вы правильное имя пользователя и пароль для аутентификации.
По сути, чтобы добавить аутентификацию к вашему веб-серверу, вам нужно добавить следующие строки после каждого запроса:
if(!request->authenticate(http_username, http_password))
return request->requestAuthentication();
Эти строки постоянно показывают окно аутентификации, пока вы не введёте правильные учётные данные.
Это нужно сделать для всех запросов. Таким образом, вы гарантируете, что получите ответы только при авторизации.
Например, когда вы пытаетесь получить доступ к корневому URL (IP-адрес ESP), вы добавляете предыдущие две строки перед отправкой страницы. Если вы введёте неправильные учётные данные, браузер будет продолжать их запрашивать.
Рекомендуем прочитать: ESP32/ESP8266 Web Server HTTP Authentication (Username and Password Protected)
Если вы обращаетесь к корневому URL / и вводите правильные учётные данные, отправляется главная веб-страница (сохранённая в переменной index_html).
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
if(!request->authenticate(http_username, http_password))
return request->requestAuthentication();
request->send_P(200, "text/html", index_html, processor);
});
Обработка выхода из системы
Когда вы нажимаете кнопку logout, ESP получает запрос по URL /logout. В этом случае отправляется код ответа 401.
server.on("/logout", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(401);
});
Код ответа 401 – это ошибка HTTP-ответа, указывающая на несанкционированный доступ, что означает невозможность аутентификации отправленного клиентом запроса. Таким образом, это будет иметь тот же эффект, что и выход из системы – запрашиваются имя пользователя и пароль, и доступ к веб-серверу не предоставляется до повторного входа.
Когда вы нажимаете кнопку logout на веб-сервере, через секунду ESP получает ещё один запрос по URL /logged-out. В этом случае отправляется HTML-текст для построения страницы выхода (переменная logout_html).
server.on("/logged-out", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", logout_html, processor);
});
Обработка выхода
Когда вы нажимаете кнопку для управления выходом, ESP получает запрос вида /output?state=<inputMessage>. Значение inputMessage может быть 0 или 1 (выключено или включено).
Следующие строки проверяют, содержит ли запрос по URL /output параметр state. Если да, значение state сохраняется в переменной inputMessage. Затем управляется GPIO выхода со значением этого сообщения digitalWrite(output, inputMessage.toInt()).
// Send a GET request to control output socket <ESP_IP>/output?state=<inputMessage>
server.on("/output", HTTP_GET, [] (AsyncWebServerRequest *request) {
if(!request->authenticate(http_username, http_password))
return request->requestAuthentication();
String inputMessage;
// GET gpio and state value
if (request->hasParam(PARAM_INPUT_1)) {
inputMessage = request->getParam(PARAM_INPUT_1)->value();
digitalWrite(output, inputMessage.toInt());
request->send(200, "text/plain", "OK");
}
request->send(200, "text/plain", "Failed");
});
Обработка статусного светодиода
При управлении статусным светодиодом инвертируется состояние кнопки.
// Send a GET request to control on board status LED <ESP_IP>/toggle
server.on("/toggle", HTTP_GET, [] (AsyncWebServerRequest *request) {
if(!request->authenticate(http_username, http_password))
return request->requestAuthentication();
ledState = !ledState;
digitalWrite(ledPin, ledState);
request->send(200, "text/plain", "OK");
});
Обработка движения
Когда вы нажимаете на карточку датчика движения после обнаружения движения, выполняется запрос по URL /clear-motion. В этом случае переменная clearMotion устанавливается в true.
server.on("/clear-motion", HTTP_GET, [] (AsyncWebServerRequest *request) {
if(!request->authenticate(http_username, http_password))
return request->requestAuthentication();
clearMotionAlert = true;
request->send(200, "text/plain", "OK");
});
Источник серверных событий
Настройка источника событий на сервере.
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
// send event with message "hello!", id current millis and set reconnect delay to 1 second
client->send("hello!",NULL,millis(),1000);
});
server.addHandler(&events);
Наконец, запустите веб-сервер.
server.begin();
Функция loop()
В loop() проверяется состояние кнопки. Если состояние кнопки изменилось, соответственно изменяется состояние светодиода и отправляется событие в браузер для обновления состояния вывода на веб-странице.
int reading = digitalRead(buttonPin);
// If the switch changed
if (reading != lastButtonState) {
// reset the debouncing timer
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
// if the button state has changed:
if (reading != buttonState) {
buttonState = reading;
// only toggle the LED if the new button state is HIGH
if (buttonState == HIGH) {
ledState = !ledState;
digitalWrite(ledPin, ledState);
events.send(String(digitalRead(ledPin)).c_str(),"led_state",millis());
}
}
}
Отправка показаний датчиков в браузер с помощью серверных событий каждые 10 секунд. Вы можете изменить этот интервал в переменной EVENT_INTERVAL_MS.
if ((millis() - lastEventTime) > EVENT_INTERVAL_MS) {
events.send("ping",NULL,millis());
events.send(String(bme.readTemperature()).c_str(),"temperature",millis());
events.send(String(bme.readHumidity()).c_str(),"humidity",millis());
events.send(String(analogRead(ldr)).c_str(),"light",millis());
lastEventTime = millis();
}
Когда движение обнаружено и уведомление не было сброшено, отправляется сообщение MOTION DETECTED в событии.
if(motionDetected & !clearMotionAlert){
events.send(String("MOTION DETECTED!").c_str(),"motion",millis());
motionDetected = false;
}
Загрузка кода
Для загрузки кода перейдите в Tools > Board и выберите DOIT ESP32 DEVKIT V1. Перейдите в Tools > Port и выберите COM-порт, к которому подключена ESP32. Затем нажмите кнопку загрузки.
Тестирование мультисенсорной платы
Откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку RST на ESP32, чтобы вывести IP-адрес ESP.
Откройте браузер и введите IP-адрес ESP32. Должна загрузиться следующая страница. Введите логин и пароль для доступа к веб-серверу. По умолчанию логин – admin, пароль – admin. Вы можете изменить их в коде.
После ввода правильных учётных данных вы получите доступ к функциям панели управления. Есть два переключателя: один для управления статусным светодиодом, другой для управления дополнительным выходом.
Вы можете управлять статусным светодиодом с помощью переключателя, а также физической кнопкой на плате. Состояние автоматически обновляется на веб-странице. Есть ещё один переключатель для управления дополнительным выходом, например модулем реле.
Рекомендуем прочитать: ESP32 Relay Module – Control AC Appliances (Web Server)
Веб-сервер отображает последние показания датчиков. Показания обновляются каждые 10 секунд автоматически с помощью серверных событий. Это означает, что когда ESP32 получает новые показания, он отправляет событие клиенту (вашему браузеру). Когда это событие происходит, поля обновляются новыми показаниями.
Наконец, есть карточка, показывающая, обнаружено ли движение. При обнаружении движения отображается сообщение «Motion Detected». Это сообщение также обновляется автоматически с помощью серверных событий.
Увидев это уведомление, вы можете нажать на карточку движения. Предупреждающее сообщение будет сброшено и вместо него отобразится «No motion».
Заключение
Мы надеемся, что этот проект оказался для вас полезным и вы сможете собрать его самостоятельно. Вы можете запрограммировать IoT Shield другим кодом, подходящим для ваших нужд. Например, вы можете управлять выходом на основе текущего значения температуры или добавить поле порога. Вы также можете отредактировать Gerber-файлы и добавить другие функции к ESP32 IoT Shield.
У нас есть другие похожие проекты, включающие проектирование и изготовление печатных плат, которые могут вас заинтересовать:
Узнайте больше о ESP32 с нашими ресурсами:
—
Источник: :doc:`ESP32 IoT Shield PCB with Dashboard for Outputs and Sensors <../esp32-iot-shield-pcb-dashboard/index>` by Rui Santos, Random Nerd Tutorials