Završni projekat: pametna učionica sa ESP32
U ovom projektu spajamo skoro sve što smo naučili: ESP32 meri temperaturu i vlažnost senzorom DHT11, svetleći prsten bojom pokazuje da li je u učionici hladno, prijatno ili toplo, servo motor otvara „prozor“ kada je pretoplo, a sve se prati i podešava sa veb strane na telefonu. Na kraju po želji dodajemo i OLED displej.
Šta ponavljamo
Pre početka ponovi lekcije u kojima je svaki deo objašnjen posebno:
- Temperatura i vlažnost – DHT11 (ESP32) – merenje i meteo stanica sa veb stranom
- Svetleći prsten (ESP32) – biblioteka Adafruit NeoPixel i boje
- Servo motor SG90 – vežba (ESP32) – biblioteka ESP32Servo i napajanje servo motora
- OLED displej – za nadogradnju na kraju
Kako učionica radi
| Temperatura | Svetleći prsten | Prozor (servo) |
|---|---|---|
| ispod 20 °C | plavo – hladno | zatvoren (0°) |
| od 20 do 26 °C | zeleno – prijatno | zatvoren (0°) |
| iznad 26 °C | crveno – toplo | otvoren (90°) |
To je automatski režim. Na veb strani može da se izabere i ručni režim: tada korisnik sam otvara i zatvara prozor i bira svetlo (belo, toplo žuto ili isključeno).
Delovi i povezivanje
| Deo | Pin dela | ESP32 |
|---|---|---|
| DHT11 | VCC / GND / DATA | 3V3 / GND / GPIO4 |
| svetleći prsten (12 dioda) | PWR / GND / IN (DIN) | VIN (5 V) / GND / GPIO27 |
| servo motor SG90 | crvena / smeđa / narandžasta | VIN (5 V) / GND / GPIO13 |
| OLED displej (nadogradnja) | VCC / GND / SDA / SCL | 3V3 / GND / GPIO21 / GPIO22 |
Napajanje: servo i prsten zajedno mogu u trenutku da povuku više struje nego što daje USB port računara. Ako se ESP32 resetuje kada servo krene, napajaj servo i prsten iz posebnog izvora od 5 V (npr. punjača za telefon) i spoji sve GND žice zajedno. Prsten drži na maloj jačini (
setBrightness(40)).

Korak 1: provera delova
Dobra navika u svakom projektu: pre glavnog programa proveri svaki deo posebno. Ovaj program boji prsten u plavo, pomera servo i ispisuje merenja u Serial Monitoru (115200). Ako nešto ne radi, sada je lako naći koji deo.
// Pametna učionica – korak 1: provera svih delova odjednom
// DHT11 -> GPIO4, svetleći prsten (DIN) -> GPIO27, servo (signal) -> GPIO13
#include <DHT.h>
#include <Adafruit_NeoPixel.h>
#include <ESP32Servo.h>
const int DHT_PIN = 4;
const int PRSTEN_PIN = 27;
const int SERVO_PIN = 13;
const int BROJ_LED = 12;
DHT dht(DHT_PIN, DHT11);
Adafruit_NeoPixel prsten(BROJ_LED, PRSTEN_PIN, NEO_GRB + NEO_KHZ800);
Servo prozor;
void setup() {
Serial.begin(115200);
dht.begin();
prsten.begin();
prsten.setBrightness(40);
prozor.setPeriodHertz(50);
prozor.attach(SERVO_PIN, 500, 2400);
prsten.fill(prsten.Color(0, 0, 255)); // prsten: plavo
prsten.show();
prozor.write(0); // prozor: zatvoren
delay(1000);
prozor.write(90); // prozor: otvoren
delay(1000);
prozor.write(0);
}
void loop() {
delay(2000);
float t = dht.readTemperature();
float h = dht.readHumidity();
if (isnan(t) || isnan(h)) {
Serial.println("DHT11: greska");
} else {
Serial.print("DHT11: ");
Serial.print(t, 1);
Serial.print(" C, ");
Serial.print(h, 0);
Serial.println(" %");
}
}
Korak 2: automatski rad
Sada temperatura odlučuje o boji prstena i položaju prozora, po tabeli sa početka. Granice su zapisane kao konstante HLADNO i TOPLO, pa ih lako menjaš. Za probu zagrej senzor dlanom ili dahom.
// Pametna učionica – korak 2: automatski rad (bez veb strane)
// temperatura određuje boju prstena i da li je prozor otvoren
#include <DHT.h>
#include <Adafruit_NeoPixel.h>
#include <ESP32Servo.h>
const int DHT_PIN = 4;
const int PRSTEN_PIN = 27;
const int SERVO_PIN = 13;
const int BROJ_LED = 12;
const float HLADNO = 20.0; // ispod ovoga: plavo
const float TOPLO = 26.0; // iznad ovoga: crveno i prozor se otvara
const int ZATVOREN = 0; // uglovi servo motora
const int OTVOREN = 90;
DHT dht(DHT_PIN, DHT11);
Adafruit_NeoPixel prsten(BROJ_LED, PRSTEN_PIN, NEO_GRB + NEO_KHZ800);
Servo prozor;
void svetlo(int r, int g, int b) {
prsten.fill(prsten.Color(r, g, b));
prsten.show();
}
void setup() {
Serial.begin(115200);
dht.begin();
prsten.begin();
prsten.setBrightness(40);
prozor.setPeriodHertz(50);
prozor.attach(SERVO_PIN, 500, 2400);
prozor.write(ZATVOREN);
}
void loop() {
delay(2000);
float t = dht.readTemperature();
if (isnan(t)) {
return; // preskačemo neuspelo merenje
}
Serial.println(t);
if (t < HLADNO) {
svetlo(0, 0, 255); // hladno
} else if (t <= TOPLO) {
svetlo(0, 255, 0); // prijatno
} else {
svetlo(255, 0, 0); // toplo
}
if (t > TOPLO) {
prozor.write(OTVOREN);
} else {
prozor.write(ZATVOREN);
}
}
Korak 3: veb strana
Završni program spaja korak 2 sa veb serverom iz meteo stanice. Upiši ime i lozinku svoje Wi-Fi mreže, pošalji program i u Serial Monitoru pročitaj adresu (npr. http://192.168.1.45). Otvori je na telefonu koji je u istoj mreži.
// Pametna učionica – završni program:
// DHT11, svetleći prsten, servo („prozor“) i veb strana
// Biblioteke: DHT sensor library, Adafruit NeoPixel, ESP32Servo (Library Manager)
#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>
#include <Adafruit_NeoPixel.h>
#include <ESP32Servo.h>
const char* SSID = "IME_MREZE"; // upiši ime Wi-Fi mreže
const char* LOZINKA = "LOZINKA"; // i lozinku
// ---- pinovi ----
const int DHT_PIN = 4;
const int PRSTEN_PIN = 27;
const int SERVO_PIN = 13;
const int BROJ_LED = 12;
// ---- podešavanja ----
const float HLADNO = 20.0;
const float TOPLO = 26.0;
const int ZATVOREN = 0;
const int OTVOREN = 90;
DHT dht(DHT_PIN, DHT11);
Adafruit_NeoPixel prsten(BROJ_LED, PRSTEN_PIN, NEO_GRB + NEO_KHZ800);
Servo prozor;
WebServer server(80);
// ---- stanje učionice ----
float temperatura = 0;
float vlaznost = 0;
bool automatski = true; // true: odlučuje temperatura, false: odlučuje korisnik
bool prozorOtvoren = false;
String svetloRucno = "iskljuceno"; // boja u ručnom režimu
unsigned long poslednjeMerenje = 0;
void postaviSvetlo(int r, int g, int b) {
prsten.fill(prsten.Color(r, g, b));
prsten.show();
}
// primenjuje stanje na prsten i servo
void primeni() {
if (automatski) {
if (temperatura < HLADNO) postaviSvetlo(0, 0, 255);
else if (temperatura <= TOPLO) postaviSvetlo(0, 255, 0);
else postaviSvetlo(255, 0, 0);
prozorOtvoren = temperatura > TOPLO;
} else {
if (svetloRucno == "bela") postaviSvetlo(255, 255, 255);
else if (svetloRucno == "zuta") postaviSvetlo(255, 150, 0);
else postaviSvetlo(0, 0, 0);
}
prozor.write(prozorOtvoren ? OTVOREN : ZATVOREN);
}
void merenje() {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) {
temperatura = t;
vlaznost = h;
}
}
// dugme na veb strani je obična veza (link) sa parametrom u adresi
String dugme(const char* adresa, const char* tekst) {
return String("<a class='d' href='") + adresa + "'>" + tekst + "</a> ";
}
void pocetnaStrana() {
String s = "<!DOCTYPE html><html><head><meta charset='utf-8'>";
s += "<meta name='viewport' content='width=device-width'>";
s += "<meta http-equiv='refresh' content='10'>";
s += "<title>Pametna ucionica</title>";
s += "<style>body{font-family:sans-serif;margin:16px}";
s += ".d{display:inline-block;padding:10px 14px;margin:4px;background:#1e6fd9;";
s += "color:#fff;border-radius:8px;text-decoration:none}</style>";
s += "</head><body><h1>Pametna ucionica</h1>";
s += "<p>Temperatura: <b>" + String(temperatura, 1) + " °C</b><br>";
s += "Vlaznost: <b>" + String(vlaznost, 0) + " %</b><br>";
s += "Prozor: <b>" + String(prozorOtvoren ? "otvoren" : "zatvoren") + "</b><br>";
s += "Rezim: <b>" + String(automatski ? "automatski" : "rucni") + "</b></p>";
s += "<h2>Rezim</h2>";
s += dugme("/rezim?r=auto", "Automatski") + dugme("/rezim?r=rucno", "Rucni");
if (!automatski) {
s += "<h2>Prozor</h2>";
s += dugme("/prozor?p=otvori", "Otvori") + dugme("/prozor?p=zatvori", "Zatvori");
s += "<h2>Svetlo</h2>";
s += dugme("/svetlo?b=bela", "Belo") + dugme("/svetlo?b=zuta", "Toplo zuto");
s += dugme("/svetlo?b=iskljuceno", "Iskljuci");
}
s += "</body></html>";
server.send(200, "text/html", s);
}
// posle komande vraćamo pregledač na početnu stranu
void nazadNaPocetnu() {
primeni();
server.sendHeader("Location", "/");
server.send(303, "text/plain", "");
}
void rezim() {
automatski = server.arg("r") == "auto";
nazadNaPocetnu();
}
void komandaProzor() {
if (!automatski) prozorOtvoren = server.arg("p") == "otvori";
nazadNaPocetnu();
}
void komandaSvetlo() {
if (!automatski) svetloRucno = server.arg("b");
nazadNaPocetnu();
}
void setup() {
Serial.begin(115200);
dht.begin();
prsten.begin();
prsten.setBrightness(40);
prozor.setPeriodHertz(50);
prozor.attach(SERVO_PIN, 500, 2400);
prozor.write(ZATVOREN);
WiFi.begin(SSID, LOZINKA);
Serial.print("Povezivanje");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Otvori u pregledacu: http://");
Serial.println(WiFi.localIP());
server.on("/", pocetnaStrana);
server.on("/rezim", rezim);
server.on("/prozor", komandaProzor);
server.on("/svetlo", komandaSvetlo);
server.begin();
merenje();
primeni();
}
void loop() {
server.handleClient();
if (millis() - poslednjeMerenje >= 2000) { // merenje na 2 s, bez delay()
poslednjeMerenje = millis();
merenje();
primeni();
}
}
| Adresa | Šta radi |
|---|---|
/ | početna strana: merenja, stanje prozora i dugmad |
/rezim?r=auto ili ?r=rucno | bira režim rada |
/prozor?p=otvori ili ?p=zatvori | otvara ili zatvara prozor (samo u ručnom režimu) |
/svetlo?b=bela, zuta, iskljuceno | bira svetlo (samo u ručnom režimu) |
Kako program radi
- Promenljive
automatski,prozorOtvorenisvetloRucnopamte stanje učionice. Funkcijaprimeni()to stanje prenosi na prsten i servo. - Dugmad na strani su obične veze. Deo adrese posle znaka
?je parametar – ESP32 ga čita naredbomserver.arg("p"). - Posle komande ESP32 odgovara kodom 303 i adresom
/, pa se pregledač vraća na početnu stranu (i ponovno osvežavanje strane ne ponavlja komandu). - Merenje ide preko
millis(), a nedelay(), da bi server stalno mogao da odgovara telefonu.
Nadogradnja: OLED displej
Dodaj displej iz lekcije OLED displej (SDA na GPIO21, SCL na GPIO22) da se stanje vidi i bez telefona. Ovaj kratki program proverava prikaz; funkcije pokreniEkran() i prikaziNaEkranu() zatim prenesi u završni program.
// Nadogradnja – stanje učionice na OLED displeju (SDA = GPIO21, SCL = GPIO22)
// Dodaj ove delove u završni program.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 ekran(128, 64, &Wire, -1);
// 1) u setup(), pre WiFi.begin():
void pokreniEkran() {
Wire.begin(21, 22);
ekran.begin(SSD1306_SWITCHCAPVCC, 0x3C);
ekran.setTextColor(SSD1306_WHITE);
}
// 2) u loop(), posle merenje() i primeni():
void prikaziNaEkranu(float t, float h, bool otvoren, bool automatski) {
ekran.clearDisplay();
ekran.setTextSize(2);
ekran.setCursor(0, 0);
ekran.print(t, 1);
ekran.print(" C");
ekran.setTextSize(1);
ekran.setCursor(0, 24);
ekran.print("Vlaznost: ");
ekran.print(h, 0);
ekran.print(" %");
ekran.setCursor(0, 38);
ekran.print(otvoren ? "Prozor: otvoren" : "Prozor: zatvoren");
ekran.setCursor(0, 52);
ekran.print(automatski ? "Rezim: auto" : "Rezim: rucni");
ekran.display();
}
void setup() {
pokreniEkran();
prikaziNaEkranu(22.5, 45, false, true); // probni prikaz
}
void loop() {
}
U završnom programu pozovi pokreniEkran() na početku setup(), a prikaziNaEkranu(temperatura, vlaznost, prozorOtvoren, automatski) u loop(), odmah posle primeni().
Kako se ocenjuje projekat
Ako projekat radiš u školi, nastavnik može da koristi ovu tabelu (ukupno 100 poena):
| Deo projekta | Poeni | Šta se gleda |
|---|---|---|
| povezivanje | 20 | sve je ispravno povezano, zajednički GND, servo i prsten na 5 V |
| automatski rad | 25 | boja prstena i prozor odgovaraju temperaturi |
| veb strana | 25 | prikazuje merenja, menja režim, prozor i svetlo |
| kod | 15 | konstante umesto „magičnih brojeva“, komentari, pregledne funkcije |
| prikaz i objašnjenje | 15 | učenik pokazuje projekat i objašnjava kako radi |
Skala ocena: 0–49 poena = 1, 50–62 = 2, 63–75 = 3, 76–88 = 4, 89–100 = 5. Skala je predlog; nastavnik može da je prilagodi.
Zadaci za vežbu
Pokušaj prvo sam da rešiš zadatak, pa tek onda otvori rešenje.
- Dodaj školsko zvono: posle 45 minuta prsten 5 puta trepne žuto. Za probu koristi 45 sekundi umesto 45 minuta.
- Prozor neka se otvara i kada je vazduh previše vlažan (vlažnost veća od 70 %), a ne samo kada je toplo. Napiši funkciju
trebaOtvoriti(t, h). - Neka se prozor otvara postepeno: na 26 °C je zatvoren (0°), na 30 °C ili više potpuno otvoren (90°), a između – srazmerno temperaturi.
Rešenje 1: školsko zvono
// Zadatak – „školsko zvono“: posle 45 minuta prsten trepne žuto 5 puta
// (ovde 45 s, radi probe)
#include <Adafruit_NeoPixel.h>
const int PRSTEN_PIN = 27;
Adafruit_NeoPixel prsten(12, PRSTEN_PIN, NEO_GRB + NEO_KHZ800);
const unsigned long CAS = 45UL * 1000; // za pravi čas: 45UL * 60 * 1000
unsigned long pocetak = 0;
void setup() {
prsten.begin();
prsten.setBrightness(40);
pocetak = millis();
}
void loop() {
if (millis() - pocetak >= CAS) {
for (int i = 0; i < 5; i++) {
prsten.fill(prsten.Color(255, 180, 0));
prsten.show();
delay(300);
prsten.clear();
prsten.show();
delay(300);
}
pocetak = millis(); // počinje novi čas
}
}
Rešenje 2: provetravanje zbog vlažnosti
// Zadatak – provetravanje i zbog vlažnosti: prozor se otvara kada je toplo
// ILI kada je vlažnost veća od 70 %
// (zameni deo „if (t > TOPLO)“ u programu iz koraka 2 ovom funkcijom)
const float TOPLO = 26.0;
const float VLAZNO = 70.0;
bool trebaOtvoriti(float t, float h) {
return t > TOPLO || h > VLAZNO;
}
void setup() {
Serial.begin(115200);
// provera funkcije na nekoliko primera
Serial.println(trebaOtvoriti(24, 50)); // 0 – prijatno
Serial.println(trebaOtvoriti(28, 50)); // 1 – toplo
Serial.println(trebaOtvoriti(22, 80)); // 1 – vlažno
}
void loop() {
}
Rešenje 3: postepeno otvaranje prozora
// Zadatak – prozor se otvara postepeno: što je toplije, to je otvoreniji
// (26 °C = 0°, 30 °C ili više = 90°)
#include <ESP32Servo.h>
Servo prozor;
int ugaoZaTemperaturu(float t) {
// množimo sa 10 da ne izgubimo decimalu (map radi sa celim brojevima)
int ugao = map((int)(t * 10), 260, 300, 0, 90);
return constrain(ugao, 0, 90);
}
void setup() {
Serial.begin(115200);
prozor.setPeriodHertz(50);
prozor.attach(13, 500, 2400);
for (float t = 24; t <= 32; t += 1) { // provera na nekoliko temperatura
Serial.print(t);
Serial.print(" C -> ");
Serial.println(ugaoZaTemperaturu(t));
}
}
void loop() {
}
Šta dalje
Završio si kurs! Proveri koliko znaš na strani Kontrolni test: Arduino i ESP32, a zatim nastavi sa oblašću Internet stvari (IoT), gde ESP32 šalje podatke na internet.
Proveri znanje
Otvori kviz
Kviz: pametna učionica
1. Zašto je svetleći prsten u projektu prebačen na GPIO27?
2. Šta treba uraditi ako se ESP32 resetuje kada servo krene?
3. Šta vraća server.arg("p") kada se otvori adresa /prozor?p=otvori?
4. Zašto se u završnom programu meri preko millis(), a ne delay(2000)?
