/* Revisi: ESP32 + SSD1306 + DHT22 + PIR wake + OpenWeather + NTP
- PIR on RTC-capable pin (GPIO33) for ext0 deep sleep wake
- Queue-safe SensorData_t (no Arduino String inside)
- Deep sleep when no motion
- Fixed for ESP32 Arduino Core 3.x API
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <ArduinoJson.h>
#include <time.h>
#include <esp_sleep.h>
#include <esp_task_wdt.h>
#include "driver/gpio.h"
// ---------------- USER CONFIG ----------------
const char* ssid = "Wokwi-GUEST"; // Ganti dengan SSID WiFi Anda
const char* password = ""; // Ganti dengan password WiFi Anda
const char* OPENWEATHER_API_KEY = "55d2233806228fa3fc5b5be287949c50";
const char* WEATHER_CITY = "Tangerang,ID";
// DHT config
#define DHT_PIN 4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
// PIR config (RTC-capable pin required for ext0 wake)
#define PIR_PIN 33 // <<< RTC-capable pin (change if needed to other RTC pin)
volatile bool pirDetected = false;
const unsigned long PIR_TIMEOUT = 30000UL; // 30s idle -> sleep
// OLED config
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Eye params
int leftEyeCenterX = 40;
int eyeCenterY = 22;
int rightEyeCenterX = 88;
int eyeRadius = 12;
int posList[8] = {-4, -2, 0, 2, 4, 2, 0, -2};
// timing
const unsigned long eyesDuration = 3000;
const unsigned long infoDuration = 4000;
const unsigned long dhtInterval = 10000;
const unsigned long weatherInterval = 600000;
// sensor / state
float weatherTemp = NAN;
char weatherMain[24] = "--";
float roomTemp = NAN;
float roomHum = NAN;
unsigned long lastDhtMillis = 0;
unsigned long lastWeatherMillis = 0;
// FreeRTOS queue-safe struct (no String)
typedef struct {
float weatherTemp;
char weatherMain[24];
float roomTemp;
float roomHum;
time_t epochTime;
} SensorData_t;
QueueHandle_t sensorQueue;
// NTP
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 7*3600, 60000);
// Watchdog config (task-level)
#define WDT_TIMEOUT_S 30
// ---------- helpers ----------
void wifiConnect() {
Serial.printf("Connecting to %s\n", ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED) {
delay(300);
Serial.print(".");
if (millis() - start > 20000) {
Serial.println("\nWiFi failed - will retry after 5s");
delay(5000);
start = millis();
WiFi.begin(ssid, password);
}
}
Serial.print("\nWiFi OK IP=");
Serial.println(WiFi.localIP());
}
void fetchWeather() {
if (WiFi.status() != WL_CONNECTED) return;
String url = "http://api.openweathermap.org/data/2.5/weather?q=" + String(WEATHER_CITY) +
"&units=metric&appid=" + String(OPENWEATHER_API_KEY);
HTTPClient http;
http.begin(url);
int code = http.GET();
if (code == 200) {
String payload = http.getString();
StaticJsonDocument<1024> doc;
DeserializationError err = deserializeJson(doc, payload);
if (!err) {
if (doc.containsKey("main") && doc["main"].containsKey("temp")) {
weatherTemp = doc["main"]["temp"].is<float>() ? doc["main"]["temp"].as<float>() : (float)doc["main"]["temp"].as<int>();
}
if (doc.containsKey("weather") && doc["weather"].is<JsonArray>() && doc["weather"][0].containsKey("main")) {
const char* wm = doc["weather"][0]["main"];
strncpy(weatherMain, wm, sizeof(weatherMain)-1);
weatherMain[sizeof(weatherMain)-1] = 0;
}
lastWeatherMillis = millis();
Serial.printf("Weather OK: %.1fC %s\n", weatherTemp, weatherMain);
} else {
Serial.println("JSON parse failed");
}
} else {
Serial.printf("Weather HTTP err: %d\n", code);
}
http.end();
}
void readDHT() {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t) && !isnan(h)) {
roomTemp = t;
roomHum = h;
Serial.printf("DHT: %.1fC %.0f%%\n", t, h);
} else {
Serial.println("DHT read failed");
}
}
// ---------- ISR ----------
void IRAM_ATTR pirISR(void* arg) {
// Just set volatile flag
pirDetected = true;
}
// ---------- Tasks ----------
void mainMonitorTask(void* pvParameters) {
// PIR pin must be RTC-capable for deep sleep wake; still can attach ISR for runtime detection
pinMode(PIR_PIN, INPUT);
gpio_install_isr_service(0);
gpio_isr_handler_add((gpio_num_t)PIR_PIN, pirISR, NULL);
// create queue
sensorQueue = xQueueCreate(5, sizeof(SensorData_t));
// WiFi/NTP/weather initial
wifiConnect();
timeClient.begin();
timeClient.update();
// initial sensor reads
dht.begin();
readDHT();
fetchWeather();
// push initial data
SensorData_t init = {weatherTemp, "", roomTemp, roomHum, (time_t)timeClient.getEpochTime()};
strncpy(init.weatherMain, weatherMain, sizeof(init.weatherMain)-1);
init.weatherMain[sizeof(init.weatherMain)-1]=0;
xQueueSend(sensorQueue, &init, portMAX_DELAY);
unsigned long lastActivity = millis();
for (;;) {
// reset watchdog
esp_task_wdt_reset();
if (pirDetected) {
pirDetected = false;
lastActivity = millis();
Serial.println("PIR motion detected - stay awake");
}
// If idle too long, go to deep sleep
if (millis() - lastActivity > PIR_TIMEOUT) {
Serial.println("No motion - entering deep sleep...");
// cleanup
display.clearDisplay();
display.display();
delay(50);
// Configure ext0 wake on PIR_PIN high (1)
// FIXED: Use esp_sleep_enable_ext0_wakeup instead of esp_deep_sleep_enable_ext0_wakeup
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIR_PIN, 1);
// small flush then sleep
esp_deep_sleep_start();
// device will restart here after wake
}
// update NTP occasionally
timeClient.update();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void sensorUpdateTask(void* pvParameters) {
// add this task to WDT
esp_task_wdt_add(NULL);
for (;;) {
// do reading only if WiFi connected
if (WiFi.status() == WL_CONNECTED) {
if (millis() - lastDhtMillis >= dhtInterval) {
lastDhtMillis = millis();
readDHT();
}
if (millis() - lastWeatherMillis >= weatherInterval) {
lastWeatherMillis = millis();
fetchWeather();
}
}
// pack & send
SensorData_t data;
data.weatherTemp = weatherTemp;
strncpy(data.weatherMain, weatherMain, sizeof(data.weatherMain)-1);
data.weatherMain[sizeof(data.weatherMain)-1]=0;
data.roomTemp = roomTemp;
data.roomHum = roomHum;
data.epochTime = (time_t)timeClient.getEpochTime();
if (sensorQueue) xQueueSend(sensorQueue, &data, pdMS_TO_TICKS(100));
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void displayTask(void* pvParameters) {
// display init already done in setup
SensorData_t cur;
unsigned long modeStart = millis();
int mode = 0; // 0 eyes, 1 info
int posIndex = 0;
unsigned long lastFrame = 0;
unsigned long lastBlink = 0;
for (;;) {
if (xQueueReceive(sensorQueue, &cur, pdMS_TO_TICKS(1000)) == pdTRUE) {
// update local copies
weatherTemp = cur.weatherTemp;
strncpy(weatherMain, cur.weatherMain, sizeof(weatherMain)-1);
weatherMain[sizeof(weatherMain)-1]=0;
roomTemp = cur.roomTemp;
roomHum = cur.roomHum;
}
unsigned long now = millis();
if (mode == 0) {
// eyes for eyesDuration
if (now - modeStart < eyesDuration) {
if (now - lastFrame >= 150) {
// animate positions
drawEyes(posList[posIndex], posList[posIndex], false);
posIndex = (posIndex + 1) % 8;
lastFrame = now;
}
// occasional blink
if (now - lastBlink > 2000 && (random(0,100) < 6)) {
drawEyes(0,0,true);
vTaskDelay(pdMS_TO_TICKS(160));
drawEyes(0,0,false);
lastBlink = now;
}
} else {
mode = 1;
modeStart = now;
}
} else {
if (now - modeStart < infoDuration) {
showInfoScreen(cur.epochTime);
} else {
mode = 0;
modeStart = now;
}
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
// ---------- drawing helpers ----------
void drawEyes(int leftOffset, int rightOffset, bool blink) {
display.clearDisplay();
if (blink) {
for (int w = 0; w < 2; w++) {
display.drawLine(28 + leftOffset, 22 + w, 52 + leftOffset, 22 + w, SSD1306_WHITE);
display.drawLine(76 + rightOffset, 22 + w, 100 + rightOffset, 22 + w, SSD1306_WHITE);
}
} else {
int lx = leftEyeCenterX + leftOffset;
display.fillCircle(lx, eyeCenterY, eyeRadius, SSD1306_WHITE);
display.drawCircle(lx, eyeCenterY, eyeRadius, SSD1306_WHITE);
int rx = rightEyeCenterX + rightOffset;
display.fillCircle(rx, eyeCenterY, eyeRadius, SSD1306_WHITE);
display.drawCircle(rx, eyeCenterY, eyeRadius, SSD1306_WHITE);
}
display.display();
}
void showInfoScreen(time_t epoch) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
if (epoch > 0) {
struct tm ti;
localtime_r(&epoch, &ti);
char timebuf[20], datebuf[20];
strftime(timebuf, sizeof(timebuf), "%H:%M:%S", &ti);
strftime(datebuf, sizeof(datebuf), "%d %b %Y", &ti);
display.setCursor(0,0);
display.printf("%s %s", timebuf, datebuf);
} else {
display.setCursor(0,0);
display.print("Time: Syncing...");
}
display.setCursor(0,12);
display.print("Tangerang, ID");
display.setCursor(0,24);
display.setTextSize(2);
if (!isnan(weatherTemp)) {
display.printf("%.0f C", weatherTemp);
} else {
display.print("-- C");
}
display.setCursor(70,24);
display.setTextSize(1);
display.print(weatherMain);
display.setCursor(0,50);
display.setTextSize(1);
if (!isnan(roomTemp) && !isnan(roomHum)) {
display.printf("Room: %.1fC %.0f%%", roomTemp, roomHum);
} else {
display.print("Room: --C --%");
}
display.display();
}
// ---------------- setup & loop ----------------
void setup() {
Serial.begin(115200);
delay(100);
Wire.begin(21, 22);
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println("SSD1306 init failed");
for(;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Init...");
display.display();
delay(800);
// DHT init
dht.begin();
// FIXED: WDT init for ESP32 Arduino Core 3.x
esp_task_wdt_config_t wdt_config = {
.timeout_ms = WDT_TIMEOUT_S * 1000,
.idle_core_mask = 0,
.trigger_panic = true
};
esp_task_wdt_init(&wdt_config);
// create tasks
xTaskCreatePinnedToCore(mainMonitorTask, "MainMonitor", 4096, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(sensorUpdateTask, "SensorUpdate", 4096, NULL, 1, NULL, 1);
xTaskCreatePinnedToCore(displayTask, "Display", 8192, NULL, 2, NULL, 1);
}
void loop() {
// idle - all work happens in tasks
vTaskDelay(pdMS_TO_TICKS(1000));
}