Στο άρθρο αυτό θα δούμε πως να φτιάξουμε έναν Web Server με τον ESP32. Αυτός Web Server θα μας δίνει πληροφορίες για το περιβάλλον του όπως για την θερμοκρασία, την υγρασία και την ατμοσφαιρική πίεση. Επίσης θα έχουμε τη δυνατότητα να του δίνουμε και να εκτελεί εντολές, που στην περίπτωσή μας είναι να ανάβει και να σβήνει κάποια Led.
Για την υλοποίηση του λογισμικού χρησιμοποιήθηκε το Visual Studio με την προσθήκη του PlatformIO. Θα δούμε στη συνέχεια τα υλικά που χρησιμοποιήθηκαν, πως αυτά συνδέθηκαν μεταξύ τους και τέλος το προγραμματιστικό κομμάτι που τα έκανε όλα αυτά να λειτουργήσουν σαν μια ενιαία κατασκευή.
Οι δεξιότητες που χρειάζεται να έχει κάποιος για να υλοποιήσει αυτή την κατασκευή είναι να μπορεί να ξεχωρίζει τα υλικά που χρειάζονται για την κατασκευή και να μπορεί να κάνει απλές κολλήσεις σε μία πλακέτα. Επίσης χρειάζεται μια εξοικείωση με το Visual Studio και με τη γλώσσα C για να προχωρήσει στον προγραμματισμό της συσκευής.
Οι φωτογραφίες της κατασκευής πιστεύω ότι θα σας βοηθήσουν.

Λειτουργία κατασκευής
Στη διαδικασία setup η κατασκευή εμφανίζει στο Serial Monitor την ip διεύθυνση με την οποία συνδέθηκε στο τοπικό wifi. Δίνοντας τη διεύθυνση αυτή στον browser του υπολογιστή ή του κινητού σου παίρνεις την εικόνα που εμφανίζεται στη φωτογραφία. Βλέπεις με τη σειρά ημερομηνία, ώρα, θερμοκρασία, υγρασία, πίεση και την κατάσταση των LED. Από τα τρία κουμπιά που ακολουθούν μπορείς να ανάψεις και να σβήσεις όπιο LED θελήσεις. Μπορείς ταυτόχρονα να λειτουργείς από πολλά σημεία, υπολογιστές ή κινητά. Η ενημέρωση της κατάστασης των αισθητήρων και των LED γίνεται αυτόματα κάθε 30 sec.
Τοπικά με κάθε σύντομο πάτημα στο κουμπί-διακόπτη ON-OFF ενεργοποιείτε το LCD και παίρνεις τις μετρήσεις της επόμενης κάρτας που φαίνεται στον browser.
Με παρατεταμένο πάτημα στο κουμπί απενεργοποιείται το LCD ή άλλως απενεργοποιείται αυτόματα σε 30 sec
Το κουμπί – διακόπτης ενεργοποιεί ένα interrupt στον ESP32, δίνει τιμές σε κάποιες μεταβλητές και τελειώνει. Οι ρουτίνες interrupt οφείλουν να είναι πολύ σύντομες. Στη συνέχεια στο βρόχο επεξεργασίας loop() εξετάζονται οι τιμές από αυτές τις μεταβλητές για να αποφασίσει η εφαρμογή αν το πάτημα ήταν σύντομο ή παρατεταμένο ή αν έφτασε σε τέλος χρόνου για να απενεργοποιήσει το LCD.
Υλικά
Τα υλικά που χρειάστηκαν είναι
ένα ESP‑32 Dev Kit C V4
τρία Led, κόκκινο,, πράσινο, κίτρινο
τρεις αντιστάσεις 220Ω για τη σύνδεση των Led
ένα LCD Serial Interface Module με I2C interface. Το LCD είναι 16 χαρακτήρων δύο γραμμών
ένα DS3231 RTC is accessed με I2C interface
ένα DHT22 Digital Humidity & Temperature Sensor Module
ένα BMP280 Atmospheric Pressure Sensor Module με I2C interface
ένας διακόπτης Push Button
και μία αντίσταση 1kΩ για τη σύνδεση του διακόπτη
Συνδέσεις
Οι συνδέσεις των Led και του διακόπτη γίνονται στα GPIO 2,3,33 και 27 όπως φαίνονται στη συνέχεια. Ο διακόπτης σε κατάσταση ON συνδέει το GPIO27 με το GND μέσω της αντίστασης 1kΩ.
LED 1 → GPIO 2
LED 2 → GPIO 4
LED 3 → GPIO 33
Διακόπτης ON-OFF → GPIO 27
Τα στοιχεία LCD, DS3231 και BMP280 συνδέονται σε κοινό I2C bus μιας και έχουν διαφορετικές διευθύνσεις. Για τη δημιουργία του I2C bus χρησιμοποιείται το GPIO21 για SDA και το GPIO22 SCL, έτσι έχουμε τις παρακάτω συνδέσεις.
SDA –> GPIO 21
SCL → GPIO 22
GND → GND
VCC → 3.3V Προσοχή χρησιμοποιούμε τα 3,3 V για VCC στους αισθητήρες και στο LCD
Συσκευές στο ίδιο bus
| Συσκευή | Πρωτόκολλο | Διεύθυνση |
|---|---|---|
| LCD I2C | I2C | συνήθως 0x27 ή 0x3F |
| DS3231 | I2C | 0x68 |
| BMP280 | I2C | 0x76 ή 0x77 |
Ο αισθητήρας DHT22 (Θερμοκρασία / Υγρασία) Χρειάζεται 1 ψηφιακό pin.
Χρησιμοποιούμε το pin GPIO 25
DATA → GPIO 25 (που είναι γενικής χρήσης)
Έτσι έχουμε τη σύνδεση
VCC -> 3.3V
DATA → GPIO 25
GND → GND
Pull-up 4.7kΩ – 10kΩ από DATA → VCC (πολλά modules την έχουν ήδη αυτή την αντίσταση)
Ο Τελικός Πίνακας συνδέσεων είναι
| Υλικό | GPIO |
|---|---|
| LED 1 | GPIO 2 |
| LED 2 | GPIO 4 |
| LED 3 | GPIO 33 |
| Διακόπτης | GPIO 27 |
| I2C SDA | GPIO 21 |
| I2C SCL | GPIO 22 |
| DHT22 DATA | GPIO 25 |
Στην εικόνα βλέπουμε τη σύνδεση των εξαρτημάτων στο ESP32 module

Λογισμικό
Νέο project
Αν δεν έχετε εγκατεστημένο το Visual Studio, μπορείτε να το κατεβάσετε από εδώ https://visualstudio.microsoft.com/vs/ . Αφού κάνετε την εγκατάσταση πηγαίνετε στα Extensions και εγκαταστήστε το PlatformIO.
Με τη χρήση του Visual Studio μπαίνετε στο PlatformIO και δημιουργείτε ένα νέο project. Θα σας ζητηθεί το περιβάλλον ανάπτυξης, γράφετε το ESP32. Κατά τη δημιουργία του νέου project θα παρατηρήσετε μια καθυστέρηση, αυτή οφείλεται στο γεγονός ότι στο PlatformIO δημιουργείται από την αρχή το περιβάλλον ανάπτυξης και έτσι γίνονται πολλά down loads.
Στο αρχείο platformio.ini θα βρείτε το περιβάλλον ανάπτυξης που δημιούργησε το PlatformIO. Εκεί εσείς θα προσθέσετε τις βιβλιοθήκες που θα χρησιμοποιηθούν για την ανάπτυξη της εφαρμογής. Το περιεχόμενο αυτού του αρχείου για την εφαρμογή μας θα είναι όπως το παρακάτω.
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
adafruit/RTClib
adafruit/DHT sensor library
adafruit/Adafruit Unified Sensor
adafruit/Adafruit BMP280 Library
bblanchon/ArduinoJson
marcoschwartz/LiquidCrystal_I2C
Κώδικας εφαρμογής
Μολονότι για το γράψιμο εφαρμογών για μικρο ελεγκτές χρησιμοποιούσα μέχρι πριν από λίγο καιρό το Ardiuno IDE, το οποίο είναι και το πρώτο ολοκληρωμένο περιβάλλον που εμφανίστηκε για προγραμματισμό σε μικροελεκτές, η τελευταία του έκδοσή η 2.3.7 έχει έναν προβληματικό down loader και δεν μου επέτρεπε να εγκαταστήσω πλακέτες για τον ESP32. Αυτό με οδήγησε να ξεκινήσω με το Visual Studio, που μου ήταν και γνωστό από άλλες εφαρμογές. Από τα Extensions εγκατέστησα το PlatformIO , το οποίο είναι πιο επαγγελματικό για τη δημιουργία τέτοιων εφαρμογών με μικρο ελεγκτές.
Τα δύο βασικά αρχεία που θα χρησιμοποιήσετε είναι το platformio.ini, που αναφέρθηκε παραπάνω και το main.cpp. Στο αρχείο main.cpp γράφετε τον κώδικα σας σε γλώσσα C σχεδόν με την ίδια δομή που γράφεται και στο Arduino IDE. Με τη δημιουργία του αρχείου main.cpp το PlatformIO δημιουργεί τη δομή όπου θα τοποθετήσεις τα τμήματα του κώδικα.
Βιβλιοθήκες που θα χρησιμοποιηθούν
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <RTClib.h>
#include <DHT.h>
#include <Adafruit_BMP280.h>
#include <time.h>
#include <LiquidCrystal_I2C.h>
#include <ArduinoJson.h>
Οι συνδέσεις που κάνατε στην πλακέτα
Στη συνέχεια ορίζεται στο πρόγραμμα τις συνδέσεις που κάνατε στην πλακέτα
// LEDs
#define LED1 2
#define LED2 4
#define LED3 33
// Push button
#define BUTTON_PIN 27
#define LONG_PRESS_TIME 1000 // ms
#define DEBOUNCE_TIME 200 // ms
// DHT22
#define DHTPIN 25
#define DHTTYPE DHT22
// I2C
#define SDA_PIN 21
#define SCL_PIN 22
Η σύνδεση με το WiFi του δικτύου σας
const char* ssid = "my ssid";
const char* password = "my password";
WebServer server(80);
RTC_DS3231 rtc;
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP280 bmp;
Global μεταβλητές
unsigned long lcdTimeout = 0;
bool lcdOn = false;
int displayMode = 0; // 0=RTC, 1=DHT22, 2=BMP280
volatile bool buttonEvent = false;
volatile unsigned long pressStartTime = 0;
volatile unsigned long lastInterruptTime = 0;
Δηλώσεις των συναρτήσεων (declarations)
int myFunction(int, int); τυπική δήλωση που τη βάζει το σύστημα για να δημιουργήσει τη δομή προγραμματισμού
void setupRTCfromNTP();
//void handleButton();
String getRTC();
String getDHT();
String getBMP();
void handleRoot();
void toggleLED(int );
void IRAM_ATTR handleButtonInterrupt();
void updateLCD();
void handleData();
String getDateGR();
String getTimeGR();
String getTemp();
String getHum();
Παράγραφος setup
Το περιεχόμενο αυτής της παραγράφου εκτελείται μία μόνο φορά κατά την εκκίνηση του συστήματος.
void setup() {
}
Παράγραφος loop
Το περιεχόμενο αυτής της παραγράφου εκτελείται συνεχώς και υλοποιεί τη λογική της εφαρμογής.
Στον κώδικα της εφαρμογής θα βρείτε πλήρες το περιεχόμενο της loop()
void loop() {
}
Ορισμός συναρτήσεων
Εδώ είναι ο χώρος που τοποθετούνται οι συναρτήσεις που υλοποιούν την εφαρμογή
// put function definitions here:
εδώ βλέπουμε μια τυπική συνάρτηση που τη συναντήσαμε και στις δηλώσεις συναρτήσεων. Την βάζει το PlatformIO στη φάση δημιουργίας του project για να μας δείξει τη δομή που θα ακολουθήσουμε στην σύνταξη του κώδικα μας.
int myFunction(int x, int y) {
return x + y;
}
Στον κώδικα της εφαρμογής θα βρείτε πλήρεις όλες τις συναρτήσεις που υλοποιούν την δική μας εφαρμογή.
Επισημάνσεις
1. Η ημερομηνία και ώρα δίνονται στο Real Time Clock από NTP (Network Time Protocol) server με τη συνάρτηση setupRTCfromNTP() στη φάση το setup() της εφαρμογής
2. Ο πίνακας routine table του ERP32 server που υλοποιούμε δίνεται στη φάση του setup() της εφαρμογής, πριν ξεκινήσουμε το server και είναι
server.on(“/”, handleRoot);
server.on(“/led1”, [](){ toggleLED(LED1); });
server.on(“/led2”, [](){ toggleLED(LED2); });
server.on(“/led3”, [](){ toggleLED(LED3); });
server.on(“/data”, handleData);
3. Η συνάρτηση void handleRoot() υλοποιεί τη δημιουργία της εικόνας στον browser και η ενημέρωση της γίνεται με τεχνική AJAX (χωρίς reload), χωρίς δηλαδή να ξαναφορτώνεται, από το scriptupdateData() κάθε 30 sec.
Ο κώδικας της εφαρμογής
Εδώ βρίσκεται το πλήρες περιεχόμενο του αρχείο main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <RTClib.h>
#include <DHT.h>
#include <Adafruit_BMP280.h>
#include <time.h>
#include <LiquidCrystal_I2C.h>
#include <ArduinoJson.h>
// LEDs
#define LED1 2
#define LED2 4
#define LED3 33
// Push button
#define BUTTON_PIN 27
#define LONG_PRESS_TIME 1000 // ms
#define DEBOUNCE_TIME 200 // ms
// DHT22
#define DHTPIN 25
#define DHTTYPE DHT22
// I2C
#define SDA_PIN 21
#define SCL_PIN 22
// LCD
LiquidCrystal_I2C lcd(0x27, 16, 2); // άλλαξε 0x27 αν χρειάζεται
const char* ssid = "your-ssid";
const char* password = "your-password";
WebServer server(80);
RTC_DS3231 rtc;
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP280 bmp;
unsigned long lcdTimeout = 0;
bool lcdOn = false;
int displayMode = 0; // 0=RTC, 1=DHT22, 2=BMP280
volatile bool buttonEvent = false;
volatile unsigned long pressStartTime = 0;
volatile unsigned long lastInterruptTime = 0;
// -----------------------------------------------------
// put function declarations here:
int myFunction(int, int);
void setupRTCfromNTP();
//void handleButton();
String getRTC();
String getDHT();
String getBMP();
void handleRoot();
void toggleLED(int );
void IRAM_ATTR handleButtonInterrupt();
void updateLCD();
void handleData();
String getDateGR();
String getTimeGR();
String getTemp();
String getHum();
// ------------------------------------------------------
void setup() {
// put your setup code here, to run once:
int result = myFunction(2, 3);
Serial.begin(115200);
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
pinMode(LED3, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(
digitalPinToInterrupt(BUTTON_PIN),
handleButtonInterrupt,
CHANGE // press + release
);
Wire.begin(SDA_PIN, SCL_PIN);
lcd.init();
lcd.backlight();
lcd.setCursor(0,0);
lcd.print("ESP32 OK");
lcd.setCursor(0,1);
lcd.print("LCD I2C 1602");
rtc.begin();
dht.begin();
bmp.begin(0x76);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
setupRTCfromNTP();
server.on("/", handleRoot);
server.on("/led1", [](){ toggleLED(LED1); });
server.on("/led2", [](){ toggleLED(LED2); });
server.on("/led3", [](){ toggleLED(LED3); });
server.on("/data", handleData);
server.begin();
Serial.println(WiFi.localIP());
}
// ------------------------------------------------------
void loop() {
// put your main code here, to run repeatedly:
if (buttonEvent) {
buttonEvent = false;
unsigned long pressDuration =
millis() - pressStartTime;
if (pressDuration >= LONG_PRESS_TIME) {
// LONG PRESS → LCD OFF
lcdOn = false;
lcd.clear();
lcd.noBacklight();
} else {
// SHORT PRESS → change screen
lcdOn = true;
lcdTimeout = millis() + 30000;
displayMode = (displayMode + 1) % 3;
updateLCD();
}
}
server.handleClient();
//handleButton();
if (lcdOn && millis() > lcdTimeout) {
lcdOn = false;
lcd.clear();
lcd.noBacklight();
}
}
// -------------------------------------------------------
// put function definitions here:
int myFunction(int x, int y) {
return x + y;
}
void setupRTCfromNTP() {
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
rtc.adjust(DateTime(
timeinfo.tm_year + 1900,
timeinfo.tm_mon + 1,
timeinfo.tm_mday,
timeinfo.tm_hour,
timeinfo.tm_min,
timeinfo.tm_sec
));
Serial.println("RTC synchronized from NTP");
} else {
Serial.println("Failed to get NTP time");
}
}
/*
void handleButton() {
static bool lastState = HIGH;
bool state = digitalRead(BUTTON_PIN);
if (lastState == HIGH && state == LOW) {
lcdOn = true;
lcdTimeout = millis() + 30000;
displayMode = (displayMode + 1) % 3;
}
lastState = state;
}
*/
String getRTC() {
DateTime now = rtc.now();
return now.timestamp(DateTime::TIMESTAMP_FULL);
}
String getDHT() {
float t = dht.readTemperature();
float h = dht.readHumidity();
return "Temp: " + String(t) + " °C<br>Hum: " + String(h) + " %";
}
String getTemp() {
float t = dht.readTemperature();
return "Temp: " + String(t);
}
String getHum() {
float h = dht.readHumidity();
return "Hum: " + String(h) + " %";
}
String getBMP() {
return String(bmp.readPressure() / 100.0) + " hPa";
}
void handleRoot() {
String page = R"rawliteral(
<!DOCTYPE html>
<html lang="el">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ESP32 Dashboard</title>
<style>
body {
font-family: Arial, sans-serif;
background: #f2f2f2;
margin: 0;
padding: 5px;
text-align: center;
}
h2 { margin-top: 5px; margin-bottom: 5px; }
h2 { font-size: 20px; }
h3 { font-size: 16px; margin: 6px 0; }
h4 { font-size: 14px; margin: 4px 0; }
.card {
background: #ffffff;
padding: 8px;
margin: 6px auto;
border-radius: 12px;
max-width: 420px;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
.status {
font-weight: bold;
font-size: 18px;
}
button {
width: 90%;
padding: 8px;
margin: 4px 0;
font-size: 14px;
border-radius: 8px;
border: none;
background: #1976d2;
color: white;
}
button:active {
background: #0d47a1;
}
</style>
</head>
<body>
<h2>ESP32 Πίνακας Ελέγχου</h2>
<div class="card">
<h4>Ημερομηνία</h4>
<div id="date">--</div>
<h4>Ώρα</h4>
<div id="time">--</div>
</div>
<div class="card">
<h4>Θερμοκρασία</h4>
<div id="dht-temp"> -- °C</div>
<h4>Υγρασία</h4>
<div id="dht-hum"> -- %</div>
</div>
<div class="card">
<h4>Πίεση</h4>
<div id="bmp">--</div>
</div>
<div class="card">
<h3>Κατάσταση LED</h3>
LED 1: <span id="l1" class="status">--</span><br>
LED 2: <span id="l2" class="status">--</span><br>
LED 3: <span id="l3" class="status">--</span>
</div>
<div class="card">
<h3>Έλεγχος LED</h3>
<button onclick="toggleLed(1)">LED 1 ON / OFF</button>
<button onclick="toggleLed(2)">LED 2 ON / OFF</button>
<button onclick="toggleLed(3)">LED 3 ON / OFF</button>
</div>
<script>
async function updateData() {
try {
const response = await fetch('/data');
const d = await response.json();
document.getElementById('date').innerText = d.date;
document.getElementById('time').innerText = d.time;
document.getElementById('dht-temp').textContent =
d.dht.temp.toFixed(1);
document.getElementById('dht-hum').textContent =
d.dht.hum.toFixed(1);
document.getElementById('bmp').innerText =
d.bmp.toFixed(1) + " hPa";
setLed('l1', d.led1);
setLed('l2', d.led2);
setLed('l3', d.led3);
} catch (e) {
console.log("AJAX error:", e);
}
}
function setLed(id, state) {
const el = document.getElementById(id);
el.innerText = state ? "ON" : "OFF";
el.style.color = state ? "green" : "red";
}
function toggleLed(n) {
fetch('/led' + n).then(() => updateData());
}
updateData();
setInterval(updateData, 30000);
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", page);
}
void toggleLED(int pin) {
digitalWrite(pin, !digitalRead(pin));
server.sendHeader("Location", "/");
server.send(303);
}
void IRAM_ATTR handleButtonInterrupt() {
unsigned long now = millis();
if (now - lastInterruptTime < DEBOUNCE_TIME) return;
if (digitalRead(BUTTON_PIN) == LOW) {
pressStartTime = now; // button pressed
} else {
buttonEvent = true; // button released
}
lastInterruptTime = now;
}
void updateLCD() {
lcd.clear();
lcd.backlight();
if (displayMode == 0) {
DateTime now = rtc.now();
lcd.print(now.day());
lcd.print("/");
lcd.print(now.month());
lcd.print("/");
lcd.print(now.year());
lcd.setCursor(0,1);
lcd.print(getTimeGR());
} else if (displayMode == 1) {
//lcd.print("DHT22:");
lcd.print(getTemp());
lcd.setCursor(0,1);
//lcd.print(getDHT());
lcd.print(getHum());
} else {
lcd.print("Pressure:");
lcd.setCursor(0,1);
lcd.print(getBMP());
}
}
void handleData() {
JsonDocument doc;
doc["date"] = getDateGR();
doc["time"] = getTimeGR();
doc["dht"]["temp"] = dht.readTemperature();
doc["dht"]["hum"] = dht.readHumidity();
doc["bmp"] = bmp.readPressure() / 100.0;
doc["led1"] = digitalRead(LED1);
doc["led2"] = digitalRead(LED2);
doc["led3"] = digitalRead(LED3);
String json;
serializeJson(doc, json);
server.send(200, "application/json", json);
}
String getDateGR() {
const char* daysGR[] = {
"Κυριακή", "Δευτέρα", "Τρίτη",
"Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο"
};
DateTime now = rtc.now();
char buf[32];
sprintf(buf, "%s %02d/%02d/%04d",
daysGR[now.dayOfTheWeek()],
now.day(),
now.month(),
now.year()
);
return String(buf);
}
String getTimeGR() {
DateTime now = rtc.now();
char buf[10];
sprintf(buf, "%02d:%02d:%02d",
now.hour(),
now.minute(),
now.second()
);
return String(buf);
}
Πολύ καλή δουλειά