Art and Technology

ESP32 Web Server – Weather station

In this article we will see how to build a Web Server with ESP32. This Web Server will give us information about its environment such as temperature, humidity and atmospheric pressure. We will also have the ability to give it and execute commands, which in our case is to turn on and off some LEDs.
Visual Studio was used to implement the software with the addition of PlatformIO. We will then see the materials used, how they were connected to each other and finally the programming part that made all of this work as a single construction.

The skills that someone needs to have to implement this construction are to be able to distinguish the materials needed for the construction and to be able to make simple soldering on a board. Also, familiarity with Visual Studio and the C language is needed to proceed with programming the device.
I believe the photos of the construction will help you.


How does construction work

During the setup process, the construction displays the IP address with which it connected to the local wifi in the Serial Monitor. By entering this address in the browser of your computer or mobile phone, you get the image shown in the photo. You see in order the date, time, temperature, humidity, pressure and the status of the LEDs. From the three buttons that follow, you can turn on and off any LED you want. You can operate from multiple locations, computers or mobile phones at the same time. The status of the sensors and LEDs is updated automatically every 30 seconds.
Locally, with each short press on the ON-OFF switch button, you activate the LCD and take the measurements of the next card shown in the browser.
With a long press on the button, the LCD is deactivated or otherwise it is automatically deactivated in 30 seconds
The switch button activates an interrupt on the ESP32, gives values ​​to some variables and ends. Then, in the loop() processing loop, the values ​​are examined to allow the application to decide whether the press was short or long and whether it timed out to turn off the LCD.


Materials

The materials needed are
an ESP‑32 Dev Kit C V4
three LEDs, red, green, yellow
three 220Ω resistors for connecting the LEDs
an LCD Serial Interface Module with I2C interface. The LCD is 16 characters and two lines
a DS3231 RTC is accessed with I2C interface
a DHT22 Digital Humidity & Temperature Sensor Module
a BMP280 Atmospheric Pressure Sensor Module with I2C interface
a Push Button switch
and a 1kΩ resistor for connecting the switch


Connections


The connections of the LEDs and the switch are made to GPIO 2,3,33 and 27 as shown below. The switch in the ON state connects GPIO27 to GND through the 1kΩ resistor.

LED 1 → GPIO 2
LED 2 → GPIO 4
LED 3 → GPIO 33
ON-OFF switch → GPIO 27


The LCD, DS3231 and BMP280 components are connected to a common I2C bus since they have different addresses. To create the I2C bus, GPIO21 is used for SDA and GPIO22 for SCL, so we have the following connections.
SDA -> GPIO 21
SCL → GPIO 22
GND → GND
VCC → 3.3V Be careful we use 3.3 V for VCC on the sensors and LCD


Devices on the same bus
Device Protocol Address
LCD I2C I2C usually 0x27 or 0x3F
DS3231 I2C 0x68
BMP280 I2C 0x76 or 0x77

The DHT22 sensor (Temperature / Humidity)
Requires 1 digital pin.
We use the GPIO 25 pin
DATA → GPIO 25 (which is general purpose)
So we have the connection
VCC -> 3.3V
DATA → GPIO 25
GND → GND
Pull-up 4.7kΩ – 10kΩ from DATA → VCC (many modules already have this resistor)


The Final Connection Board is

Hardware – GPIO
LED 1 — GPIO 2
LED 2 — GPIO 4
LED 3 — GPIO 33
Switch — GPIO 27
I2C SDA – GPIO 21
I2C SCL – GPIO 22
DHT22 DATA – GPIO 25

Software
New project

If you do not have Visual Studio installed, you can download it from here https://visualstudio.microsoft.com/vs/ . After installing, go to Extensions and install PlatformIO.
Using Visual Studio, you enter PlatformIO and create a new project. You will be asked for the development environment, write ESP32. When creating the new project, you will notice a delay, this is due to the fact that in PlatformIO the development environment is created from scratch and thus many downloads are made.
In the platformio.ini file you will find the development environment created by PlatformIO. There you will add the libraries that will be used to develop the application. The content of this file for our application will be as follows.

[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


Application code

Although until recently I was using the Ardiuno IDE, which is the first integrated environment that appeared for programming microcontrollers, its latest version 2.3.7 has a problematic download loader and did not allow me to install boards for the ESP32. This led me to start with Visual Studio, which was also familiar to me from other applications. From the Extensions I installed PlatformIO, which is more professional for creating such applications with microcontrollers.
The two main files you will use are platformio.ini, mentioned above, and main.cpp. In the main.cpp file you write your code in C language with almost the same structure as written in the Arduino IDE. By creating the main.cpp file, PlatformIO creates the structure where you will place the parts of the code.


Libraries to be used

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

The connections you made to the board

Next, the connections you made to the board are defined in the program
// 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


Connecting to your network’s 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 variables

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;


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

Remaining structure

Setup Paragraph
The contents of this paragraph are executed only once during system startup.
void setup() {
}

Loop Paragraph
The content of this paragraph is executed continuously and implements the logic of the application.
In the application code you will find the complete content of loop()
void loop() {
}

Function definitions
This is the place where the functions that implement the application are placed
// put function definitions here:
Here we see a typical function that we also encountered in function declarations. PlatformIO places it in the project creation phase to show us the structure that we will follow in writing our code.
int myFunction(int x, int y) {
return x + y;
}
In the application code you will find all the complete functions that implement the application.


Notes

1. The date and time are given to the Real Time Clock by the NTP (Network Time Protocol) server with the setupRTCfromNTP() function in the setup() phase of the application
2. The routine table of the ERP32 server that we implement is given in the setup() phase of the application, before starting the server and is
server.on(“/”, handleRoot);
server.on(“/led1”, [](){ toggleLED(LED1); });
server.on(“/led2”, [](){ toggleLED(LED2); });
server.on(“/led3”, [](){ toggleLED(LED3); });
server.on(“/data”, handleData);
3. The void handleRoot() function implements the creation of the image in the browser and its update is done using the AJAX technique (without reload), i.e. without being reloaded by the updateData() script every 30 sec.

Full application code

Here is the complete content of the main.cpp file

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

Leave a Reply

Your email address will not be published. Required fields are marked *