Završni projekat: pametna učionica sa ESP32

Lekcija 27 od 28 · Arduino i 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:

Kako učionica radi

TemperaturaSvetleći prstenProzor (servo)
ispod 20 °Cplavo – hladnozatvoren (0°)
od 20 do 26 °Czeleno – prijatnozatvoren (0°)
iznad 26 °Ccrveno – toplootvoren (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

DeoPin delaESP32
DHT11VCC / GND / DATA3V3 / GND / GPIO4
svetleći prsten (12 dioda)PWR / GND / IN (DIN)VIN (5 V) / GND / GPIO27
servo motor SG90crvena / smeđa / narandžastaVIN (5 V) / GND / GPIO13
OLED displej (nadogradnja)VCC / GND / SDA / SCL3V3 / GND / GPIO21 / GPIO22
Pazi na pinove: u lekciji o prstenu signal je bio na GPIO4, ali GPIO4 sada koristi DHT11. Zato prsten prebacujemo na GPIO27.

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)).
ESP32 i senzor DHT11 povezani na eksperimentalnoj ploči
Prvi deo projekta: ESP32 i DHT11
Šema povezivanja svetlećeg prstena sa ESP32
Svetleći prsten – u projektu signal ide na GPIO27

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) + " &deg;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=rucnobira režim rada
/prozor?p=otvori ili ?p=zatvoriotvara ili zatvara prozor (samo u ručnom režimu)
/svetlo?b=bela, zuta, iskljucenobira svetlo (samo u ručnom režimu)

Kako program radi

  • Promenljive automatski, prozorOtvoren i svetloRucno pamte stanje učionice. Funkcija primeni() to stanje prenosi na prsten i servo.
  • Dugmad na strani su obične veze. Deo adrese posle znaka ? je parametar – ESP32 ga čita naredbom server.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 ne delay(), 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 projektaPoeniŠta se gleda
povezivanje20sve je ispravno povezano, zajednički GND, servo i prsten na 5 V
automatski rad25boja prstena i prozor odgovaraju temperaturi
veb strana25prikazuje merenja, menja režim, prozor i svetlo
kod15konstante umesto „magičnih brojeva“, komentari, pregledne funkcije
prikaz i objašnjenje15uč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.

  1. Dodaj školsko zvono: posle 45 minuta prsten 5 puta trepne žuto. Za probu koristi 45 sekundi umesto 45 minuta.
  2. 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).
  3. 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)?