Skocz do zawartości

Prototyp układu sterowania oświetleniem LED


Pomocna odpowiedź

Napisano (edytowany)

W tym artykule chciałbym przedstawić prototyp układu do sterowania oświetleniem wraz z możliwością pomiarów warunków atmosferycznych. Urządzenie łączy w sobie funkcje stacji pogodowej, zegara czasu rzeczywistego, inteligentnego oświetlenia oraz systemu alarmowego z powiadomieniami przez stronę WWW. Sercem układu jest popularny i wydajny mikrokontroler ESP32, który dzięki wbudowanemu modułowi Wi-Fi umożliwia zdalne sterowanie i podgląd parametrów z dowolnego urządzenia w sieci domowej.

Konstrukcja opiera się na kilku kluczowych modułach:

  • Mikrokontroler ESP32: Mózg operacji. Zarządza logiką wszystkich peryferiów, obsługuje serwer WWW oraz komunikację I2C i cyfrową.
  • Moduł RTC DS1302: Zewnętrzny zegar czasu rzeczywistego z podtrzymaniem bateryjnym (CR2032). Dzięki niemu system pamięta dokładną datę i godzinę nawet po całkowitym odłączeniu zasilania, co eliminuje konieczność ręcznego ustawiania czasu po każdym restarcie.
  • Wyświetlacz OLED 0.96": Ekran graficzny pracujący na magistrali I2C. Pełni rolę interfejsu użytkownika, wyświetlając w trybie spoczynku aktualną godzinę, datę, temperaturę, wilgotność oraz adres IP urządzenia. W momencie wykrycia zagrożenia zmienia się w ekran ostrzegawczy.
  • Czujnik ruchu PIR: Odpowiada za detekcję obecności osób w pomieszczeniu. Jego zadziałanie wyzwala sekwencję alarmową.
  • Czujnik DHT11: Cyfrowy sensor mierzący temperaturę i wilgotność powietrza, co pozwala na bieżące monitorowanie warunków klimatycznych w pomieszczeniu.
  • PrzekaźnikElement wykonawczy pozwalający sterować urządzeniami wysokiego napięcia za pomocą sygnałów logicznych z mikrokontrolera.
  • Brzęczyk z generatorem PWM: Zapewnia sygnalizację dźwiękową (trzykrotne piknięcie) w momencie wykrycia ruchu lub załączenia lampy.

image.thumb.png.ee29f6c015e86ba899aeaa5b5f90e02c.pngimage.thumb.png.449f693ba54ebce1b19235692a352a6d.pngimage.thumb.png.6e6cd172f0189cf6eb32ebc7d27ffc52.png
 

Zasada działania i logika programu

System działa w dwóch, płynnie przełączających się trybach: automatycznym oraz manualnym (zdalnym).

W trybie domyślnym (automatycznym) urządzenie nieustannie monitoruje otoczenie za pomocą czujnika PIR. Gdy w polu widzenia sensora pojawi się ruch, mikrokontroler natychmiast podejmuje działania: aktywuje przekaźnik (włączając oświetlenie), generuje sekwencję dźwiękową przez brzęczyk oraz zmienia treść na wyświetlaczu OLED na duży, ostrzegawczy napis "WYKRYTO RUCH". Lampa pozostaje włączona przez zaprogramowany czas (w tym przypadku 5 sekund) od ostatniego wykrycia ruchu. Dzięki zastosowaniu programowania opartego na funkcji millis(), system nie "zamraża się" podczas świecenia lampy, lecz nadal w tle obsługuje serwer WWW i pomiary temperatury.

Drugą warstwą funkcjonalności jest własny serwer WWW. ESP32 generuje interaktywną stronę internetową, dostępną pod lokalnym adresem IP. Strona ta, dzięki technologii AJAX/Auto-refresh, odświeża się automatycznie, prezentując na żywo odczyty z zegara RTC, termometru oraz status czujnika ruchu. Co istotne, panel WWW pozwala na ręczne przejęcie kontroli nad oświetleniem. Użytkownik może kliknąć przycisk na stronie, aby włączyć lampę na stałe (tryb manualny), ignorując czujnik ruchu. Wyłączenie lampy z poziomu strony przywraca system do trybu automatycznego czuwania. 

Film z działania całego układu w dwóch trybach

Kod programu:

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ThreeWire.h>  
#include <RtcDS1302.h>
#include <DHT.h>
#include <WiFi.h>      
#include <WebServer.h> 


const char* ssid = "wifi";    
const char* password = "haslo wifi";

WebServer server(80); 

// --- KONFIGURACJA EKRANU OLED ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// --- KONFIGURACJA ZEGARA RTC DS1302 ---
ThreeWire myWire(12, 14, 26); // DAT, CLK, RST
RtcDS1302<ThreeWire> Rtc(myWire);

// --- KONFIGURACJA CZUJNIKA DHT11 ---
#define DHTPIN 18     
#define DHTTYPE DHT11 
DHT dht(DHTPIN, DHTTYPE);

float temperatura = 0.0;
float wilgotnosc = 0.0;
unsigned long czasOstatniegoPomiaruDHT = 0;
const long OKRES_POMIARU_DHT = 2000; 

// --- KONFIGURACJA PINÓW ---
const int PIN_CZUJNIKA = 25;       
const int PIN_PRZEKAZNIKA_K2 = 27; 
const int PIN_BRZECZYKA = 33;      

// --- KONFIGURACJA LOGIKI ---
const unsigned long CZAS_SWIECENIA = 5000; 
unsigned long czasOstatniegoRuchu = 0; // Kiedy ostatnio czujnik widział ruch
bool lampaWlaczona = false;
bool trybManualny = false; 

// ==========================================
// === FUNKCJE STRONY INTERNETOWEJ (HTML) ===
// ==========================================

void handleRoot() {
  RtcDateTime now = Rtc.GetDateTime();
  char timeStr[10];
  snprintf(timeStr, sizeof(timeStr), "%02u:%02u:%02u", now.Hour(), now.Minute(), now.Second());
  char dateStr[16];
  snprintf(dateStr, sizeof(dateStr), "%02u/%02u/%04u", now.Day(), now.Month(), now.Year());

  // --- LOGIKA WYŚWIETLANIA STANU RUCHU NA WWW ---
  // Sprawdzamy, czy ruch był wykryty w ciągu ostatnich 4000ms (4s)
  // Dzięki temu napis na stronie utrzyma się wystarczająco długo, by odświeżanie go wyłapało.
  bool ruchDlaWWW = (millis() - czasOstatniegoRuchu < 4000);

  String html = "<!DOCTYPE html><html><head>";
  html += "<meta charset='UTF-8'>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";
  // Odświeżanie strony co 2 sekundy (przyspieszyłem dla lepszej reakcji)
  html += "<meta http-equiv='refresh' content='2'>"; 
  html += "<title>Panel ESP32</title>";
  html += "<style>";
  html += "body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; text-align: center; background-color: #1a1a1a; color: #ecf0f1; margin: 0; padding: 20px; }";
  html += ".container { max-width: 400px; margin: 0 auto; }";
  html += ".box { border: 1px solid #333; margin-bottom: 20px; padding: 20px; border-radius: 15px; background-color: #2c3e50; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }";
  html += "h1 { color: #f39c12; font-size: 24px; margin-bottom: 20px; }";
  html += "h2 { font-size: 48px; margin: 10px 0; font-weight: 300; }";
  html += ".date { color: #bdc3c7; font-size: 18px; }";
  html += ".sensor-row { display: flex; justify-content: space-around; font-size: 1.2rem; margin: 15px 0; }";
  html += ".status-motion { font-size: 20px; padding: 10px; border-radius: 5px; font-weight: bold; margin-top: 10px; }";
  html += ".motion-yes { background-color: #c0392b; color: white; animation: pulse 1s infinite; }";
  html += ".motion-no { background-color: #27ae60; color: white; }";
  html += "@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.8; } 100% { opacity: 1; } }";
  html += ".btn { background-color: #2980b9; color: white; padding: 15px 0; width: 100%; text-decoration: none; display: block; font-size: 18px; border-radius: 8px; border: none; cursor: pointer; transition: background 0.3s; }";
  html += ".btn:hover { background-color: #3498db; }";
  html += ".btn-off { background-color: #e74c3c; }";
  html += ".btn-off:hover { background-color: #c0392b; }";
  html += ".ip-addr { color: #7f8c8d; font-size: 12px; margin-top: 20px; }";
  html += "</style></head><body>";
  
  html += "<div class='container'>";
  html += "<h1>Panel Sterowania Domem</h1>";
  
  // Zegar
  html += "<div class='box'>";
  html += "<h2>" + String(timeStr) + "</h2>";
  html += "<p class='date'>" + String(dateStr) + "</p>";
  html += "</div>";

  // Czujniki
  html += "<div class='box'>";
  html += "<div class='sensor-row'>";
  html += "<div>TEMP<br><b>" + String((int)temperatura) + "&deg;C</b></div>";
  html += "<div>WILG<br><b>" + String((int)wilgotnosc) + "%</b></div>";
  html += "</div>";
  
  // Status Ruchu
  if (ruchDlaWWW) {
    html += "<div class='status-motion motion-yes'>WYKRYTO RUCH!</div>";
  } else {
    html += "<div class='status-motion motion-no'>Brak ruchu</div>";
  }
  html += "</div>";

  // Lampa
  html += "<div class='box'>";
  if (lampaWlaczona) {
    html += "<p>Lampa: <b>WŁĄCZONA</b> ";
    if (trybManualny) html += "(Ręcznie)</p>";
    else html += "(Auto)</p>";
    html += "<a href='/toggle'><button class='btn btn-off'>WYŁĄCZ LAMPĘ</button></a>";
  } else {
    html += "<p>Lampa: <b>WYŁĄCZONA</b></p>";
    html += "<a href='/toggle'><button class='btn'>WŁĄCZ LAMPĘ</button></a>";
  }
  html += "</div>";
  
  html += "<p class='ip-addr'>IP: " + WiFi.localIP().toString() + "</p>";
  html += "</div>"; // End container
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleToggle() {
  if (lampaWlaczona) {
    lampaWlaczona = false;
    trybManualny = false;
    digitalWrite(PIN_PRZEKAZNIKA_K2, HIGH); // OFF
  } else {
    lampaWlaczona = true;
    trybManualny = true;
    digitalWrite(PIN_PRZEKAZNIKA_K2, LOW); // ON
  }
  server.sendHeader("Location", "/");
  server.send(303);
}

// ==========================================
// =============== SETUP ====================
// ==========================================

void setup() {
  Serial.begin(115200);
  
  // Piny
  pinMode(PIN_CZUJNIKA, INPUT);
  pinMode(PIN_PRZEKAZNIKA_K2, OUTPUT);
  digitalWrite(PIN_PRZEKAZNIKA_K2, HIGH); 

  ledcAttach(PIN_BRZECZYKA, 2000, 8); 
  ledcWrite(PIN_BRZECZYKA, 0);

  // OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    Serial.println(F("Błąd OLED"));
    for(;;);
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  
  display.setTextSize(1);
  display.setCursor(0,0);
  display.println("Laczenie WiFi...");
  display.println(ssid);
  display.display();

  WiFi.begin(ssid, password);
  int retry = 0;
  while (WiFi.status() != WL_CONNECTED && retry < 20) {
    delay(500);
    Serial.print(".");
    retry++;
  }
  
  display.clearDisplay();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nWiFi Polaczone!");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("Blad WiFi");
  }

  server.on("/", handleRoot);
  server.on("/toggle", handleToggle);
  server.begin();

  dht.begin();
  Rtc.Begin();
  if (!Rtc.IsDateTimeValid()) {
      Rtc.SetDateTime(RtcDateTime(__DATE__, __TIME__));
  }
  if (Rtc.GetIsWriteProtected()) Rtc.SetIsWriteProtected(false);
  if (!Rtc.GetIsRunning()) Rtc.SetIsRunning(true);
}

// ==========================================
// =============== LOOP =====================
// ==========================================

void loop() {
  server.handleClient();

  int odczytPIR = digitalRead(PIN_CZUJNIKA);
  unsigned long aktualnyCzas = millis();

  // --- KLUCZOWA POPRAWKA ---
  // Aktualizujemy czas ostatniego ruchu ZAWSZE gdy czujnik widzi ruch.
  // Dzięki temu strona WWW będzie wiedziała, że "przed chwilą" był ruch.
  if (odczytPIR == HIGH) {
    czasOstatniegoRuchu = aktualnyCzas;
  }

  // Odczyt DHT
  if (aktualnyCzas - czasOstatniegoPomiaruDHT >= OKRES_POMIARU_DHT) {
    czasOstatniegoPomiaruDHT = aktualnyCzas;
    float t = dht.readTemperature();
    float h = dht.readHumidity();
    if (!isnan(t) && !isnan(h)) {
      temperatura = t;
      wilgotnosc = h;
    }
  }

  RtcDateTime now = Rtc.GetDateTime();

  // --- LOGIKA LAMPY (AUTO) ---
  if (!trybManualny) {
    // Sprawdzamy, czy czas od ostatniego ruchu jest mniejszy niż czas świecenia
    // ALE warunek włączenia (trigger) robimy tylko gdy lampa jest zgaszona
    
    bool czyPowinnaSwiecic = (aktualnyCzas - czasOstatniegoRuchu < CZAS_SWIECENIA);
    
    // Jeśli czujnik "świeży" (czas < 5s) i lampa wyłączona -> Włączamy
    if (czyPowinnaSwiecic && !lampaWlaczona) {
        Serial.println("PIR: RUCH! -> Włączam Lampę");
        digitalWrite(PIN_PRZEKAZNIKA_K2, LOW); 
        lampaWlaczona = true;

        // Piknięcie
        for (int i = 0; i < 3; i++) {
          ledcWrite(PIN_BRZECZYKA, 128); delay(100);                    
          ledcWrite(PIN_BRZECZYKA, 0);   delay(100);                    
        }
    }

    // Jeśli czas minął i lampa włączona -> Wyłączamy
    if (!czyPowinnaSwiecic && lampaWlaczona) {
      Serial.println("AUTO: Koniec czasu -> Wyłączam Lampę");
      digitalWrite(PIN_PRZEKAZNIKA_K2, HIGH); 
      lampaWlaczona = false;
    }
  }

  // --- EKRAN OLED ---
  display.clearDisplay();
  
  if (lampaWlaczona) {
    display.setTextSize(2);             
    display.setCursor(10, 5);
    if (trybManualny) display.println(F("RECZNY"));
    else display.println(F("WYKRYTO"));

    display.setCursor(30, 25);
    if (trybManualny) display.println(F("ON (WWW)"));
    else display.println(F("RUCH!"));
    
    display.setTextSize(1);
    display.setCursor(15, 55);
    display.println(WiFi.localIP());

  } else {
    char timeStr[10];
    snprintf(timeStr, sizeof(timeStr), "%02u:%02u:%02u", now.Hour(), now.Minute(), now.Second());
    char dateStr[16];
    snprintf(dateStr, sizeof(dateStr), "%02u/%02u/%04u", now.Day(), now.Month(), now.Year());
    
    display.setTextSize(1);
    display.setCursor(35, 0); display.println(dateStr);
    display.setTextSize(2);
    display.setCursor(15, 15); display.println(timeStr);
    display.drawLine(0, 35, 128, 35, SSD1306_WHITE);
    display.setTextSize(1);
    display.setCursor(5, 40); display.print("T:"); display.print((int)temperatura); display.print("C");
    display.setCursor(65, 40); display.print("H:"); display.print((int)wilgotnosc); display.print("%");
    
    display.setCursor(20, 55);
    display.print("IP:"); display.println(WiFi.localIP());
  }

  display.display(); 
  delay(50); 
}

 

Edytowano przez Treker
Poprawiłem formatowanie.
  • Lubię! 2

Podoba Ci się ten projekt? Zostaw pozytywny komentarz i daj znać autorowi, że zbudował coś fajnego!

Masz uwagi? Napisz kulturalnie co warto zmienić. Doceń pracę autora nad konstrukcją oraz opisem.

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...