/* ESP32-S3 Mini - Final version (user requested pins)
- OLED SDA = GPIO4, SCL = GPIO5
- PIR = GPIO7 (RTC-capable) -> ext0 wake on HIGH
- DHT22 = GPIO6
- Buzzer/LED = GPIO10
- Battery ADC = GPIO11 (with voltage divider Rtop=100k, Rbottom=100k)
- OpenWeather One Call API 3.0 (Celsius) + NTP + deep sleep on idle
- Icons for weather, battery/WiFi always at top in info mode and overlaid in animation
- Only 2 modes: eyes animation (with top bar overlay) and full info
- Remember to replace OPENWEATHER_API_KEY, ssid, password
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <U8g2lib.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 <stdlib.h> // for random
// ---------------- USER CONFIG ----------------
const char* ssid = "Wokwi-GUEST"; // Ganti
const char* password = ""; // Ganti
const char* OPENWEATHER_API_KEY = "55d2233806228fa3fc5b5be287949c50"; // Ganti
const float LAT = -6.1783f;
const float LON = 106.6319f;
// Pin mapping (final requested)
#define OLED_SDA_PIN 4
#define OLED_SCL_PIN 5
#define PIR_PIN 7
#define DHT_PIN 6
#define BUZZER_PIN 10
#define BAT_ADC_PIN 11
// Battery divider (adjust if your resistor values differ)
const float R_TOP = 100000.0f; // ohm
const float R_BOTTOM = 100000.0f; // ohm
// DHT config
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
// PIR config
volatile bool pirDetected = false;
const unsigned long PIR_TIMEOUT = 30000UL; // 30s idle -> sleep
// U8g2 OLED config
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8X8_PIN_NONE, OLED_SCL_PIN, OLED_SDA_PIN);
// 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;
const unsigned long battInterval = 15000;
// sensor / state
float weatherTemp = NAN;
char weatherMain[24] = "--";
float weatherHigh = NAN;
float weatherLow = NAN;
float roomTemp = NAN;
float roomHum = NAN;
float batteryVoltage = NAN;
int batteryPercent = -1;
unsigned long lastDhtMillis = 0;
unsigned long lastWeatherMillis = 0;
unsigned long lastBattMillis = 0;
// FreeRTOS queue-safe struct (no String)
typedef struct {
float weatherTemp;
char weatherMain[24];
float weatherHigh;
float weatherLow;
float roomTemp;
float roomHum;
float batteryVoltage;
int batteryPercent;
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
// ---------- drawing helpers ----------
void drawTopBar(int percent, bool connected) {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setDrawColor(1);
// Battery
if (percent >= 0) {
u8g2.drawFrame(100, 1, 24, 8);
u8g2.drawFrame(124, 3, 2, 4);
int fillw = (percent * 22) / 100;
u8g2.drawBox(101, 2, fillw, 6);
char pbuf[5];
snprintf(pbuf, 5, "%d%%", percent);
u8g2.setFont(u8g2_font_4x6_tr);
u8g2.drawStr(105, 8, pbuf);
u8g2.setFont(u8g2_font_6x10_tf);
}
// WiFi symbol if connected
if (connected) {
// Simple wifi arcs/lines
u8g2.drawLine(64, 8, 67, 5);
u8g2.drawLine(66, 8, 69, 5);
u8g2.drawLine(68, 8, 70, 5);
}
}
void drawWeatherIcon(int x, int y, const char* main) {
u8g2.setDrawColor(1);
bool isRain = (strstr(main, "Rain") != NULL) || (strcmp(main, "Drizzle") == 0);
if (strcmp(main, "Clear") == 0) {
// Sun
u8g2.drawCircle(x + 10, y + 8, 4);
// Rays
u8g2.drawLine(x + 10, y + 2, x + 10, y - 1);
u8g2.drawLine(x + 10, y + 14, x + 10, y + 17);
u8g2.drawLine(x + 2, y + 8, x - 1, y + 8);
u8g2.drawLine(x + 18, y + 8, x + 21, y + 8);
} else if (strcmp(main, "Clouds") == 0) {
// Cloud
u8g2.drawCircle(x + 5, y + 8, 3);
u8g2.drawCircle(x + 12, y + 6, 4);
u8g2.drawCircle(x + 19, y + 8, 3);
} else if (isRain) {
// Cloud
u8g2.drawCircle(x + 5, y + 8, 3);
u8g2.drawCircle(x + 12, y + 6, 4);
u8g2.drawCircle(x + 19, y + 8, 3);
// Rain drops
u8g2.drawLine(x + 6, y + 12, x + 6, y + 18);
u8g2.drawLine(x + 12, y + 10, x + 12, y + 16);
u8g2.drawLine(x + 18, y + 12, x + 18, y + 18);
} else {
// Default: question mark or empty
u8g2.drawStr(x + 6, y + 10, "?");
}
}
void drawEyesContent(int leftOffset, int rightOffset, bool blink) {
u8g2.setDrawColor(1);
if (blink) {
for (int w = 0; w < 2; w++) {
u8g2.drawLine(28 + leftOffset, 22 + w, 52 + leftOffset, 22 + w);
u8g2.drawLine(76 + rightOffset, 22 + w, 100 + rightOffset, 22 + w);
}
} else {
int lx = leftEyeCenterX + leftOffset;
u8g2.drawDisc(lx, eyeCenterY, eyeRadius);
u8g2.drawCircle(lx, eyeCenterY, eyeRadius);
int rx = rightEyeCenterX + rightOffset;
u8g2.drawDisc(rx, eyeCenterY, eyeRadius);
u8g2.drawCircle(rx, eyeCenterY, eyeRadius);
}
}
// ---------- 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 = "https://api.openweathermap.org/data/3.0/onecall?lat=" +
String(LAT, 6) + "&lon=" + String(LON, 6) +
"&exclude=hourly,minutely,alerts&appid=" + String(OPENWEATHER_API_KEY) +
"&units=metric";
HTTPClient http;
http.begin(url);
int code = http.GET();
if (code == 200) {
String payload = http.getString();
StaticJsonDocument<2048> doc; // Larger for One Call
DeserializationError err = deserializeJson(doc, payload);
if (!err) {
// Current
if (doc["current"].containsKey("temp")) {
weatherTemp = doc["current"]["temp"].as<float>();
}
if (doc["current"].containsKey("weather") && doc["current"]["weather"].is<JsonArray>() &&
doc["current"]["weather"].size() > 0 && doc["current"]["weather"][0].containsKey("main")) {
const char* wm = doc["current"]["weather"][0]["main"];
strncpy(weatherMain, wm ? wm : "--", sizeof(weatherMain) - 1);
weatherMain[sizeof(weatherMain) - 1] = 0;
}
// Daily[0] for today high/low
if (doc["daily"].is<JsonArray>() && doc["daily"].size() > 0) {
JsonObject day = doc["daily"][0];
if (day["temp"].containsKey("max")) {
weatherHigh = day["temp"]["max"].as<float>();
}
if (day["temp"].containsKey("min")) {
weatherLow = day["temp"]["min"].as<float>();
}
}
lastWeatherMillis = millis();
Serial.printf("Weather OK: %.1fC %s H:%.1f L:%.1f\n", weatherTemp, weatherMain, weatherHigh, weatherLow);
} 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");
}
}
void readBattery() {
// ADC setup done in setup()
int raw = analogRead(BAT_ADC_PIN);
// 12-bit resolution assumed (0..4095)
float adcMax = 4095.0f;
float vref = 3.3f; // default Vref for ADC on board
float measured = (raw / adcMax) * vref;
// voltage before divider
float vin = measured * ((R_TOP + R_BOTTOM) / R_BOTTOM);
batteryVoltage = vin;
// map voltage to percent (simple linear mapping 3.3V -> 0% ; 4.2V -> 100%)
float vmin = 3.3f;
float vmax = 4.20f;
float pct = (vin - vmin) / (vmax - vmin) * 100.0f;
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
batteryPercent = (int)round(pct);
Serial.printf("Battery ADC raw=%d V=%.3fV %d%%\n", raw, batteryVoltage, batteryPercent);
}
// ---------- ISR ----------
void IRAM_ATTR pirISR() {
pirDetected = true;
}
// ---------- Tasks ----------
void mainMonitorTask(void* pvParameters) {
// attach runtime ISR for PIR
pinMode(PIR_PIN, INPUT);
attachInterrupt(PIR_PIN, pirISR, RISING);
// create queue
sensorQueue = xQueueCreate(5, sizeof(SensorData_t));
// WiFi/NTP/weather initial
wifiConnect();
timeClient.begin();
timeClient.update();
// initial sensor reads
dht.begin();
readDHT();
fetchWeather();
readBattery();
// push initial data
SensorData_t initData;
initData.weatherTemp = weatherTemp;
strncpy(initData.weatherMain, weatherMain, sizeof(initData.weatherMain) - 1);
initData.weatherMain[sizeof(initData.weatherMain) - 1] = 0;
initData.weatherHigh = weatherHigh;
initData.weatherLow = weatherLow;
initData.roomTemp = roomTemp;
initData.roomHum = roomHum;
initData.batteryVoltage = batteryVoltage;
initData.batteryPercent = batteryPercent;
initData.epochTime = (time_t)timeClient.getEpochTime();
xQueueSend(sensorQueue, &initData, 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");
digitalWrite(BUZZER_PIN, HIGH);
vTaskDelay(pdMS_TO_TICKS(80));
digitalWrite(BUZZER_PIN, LOW);
}
// If idle too long, go to deep sleep
if (millis() - lastActivity > PIR_TIMEOUT) {
Serial.println("No motion - entering deep sleep...");
// cleanup
u8g2.clearBuffer();
u8g2.sendBuffer();
delay(50);
// Configure ext0 wake on PIR_PIN HIGH (1)
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIR_PIN, 1);
// small flush then deep 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 (current task)
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();
}
}
if (millis() - lastBattMillis >= battInterval) {
lastBattMillis = millis();
readBattery();
}
// pack & send
SensorData_t data;
data.weatherTemp = weatherTemp;
strncpy(data.weatherMain, weatherMain, sizeof(data.weatherMain) - 1);
data.weatherMain[sizeof(data.weatherMain) - 1] = 0;
data.weatherHigh = weatherHigh;
data.weatherLow = weatherLow;
data.roomTemp = roomTemp;
data.roomHum = roomHum;
data.batteryVoltage = batteryVoltage;
data.batteryPercent = batteryPercent;
data.epochTime = (time_t)timeClient.getEpochTime();
if (sensorQueue) xQueueSend(sensorQueue, &data, pdMS_TO_TICKS(100));
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void displayTask(void* pvParameters) {
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;
weatherHigh = cur.weatherHigh;
weatherLow = cur.weatherLow;
roomTemp = cur.roomTemp;
roomHum = cur.roomHum;
batteryVoltage = cur.batteryVoltage;
batteryPercent = cur.batteryPercent;
}
unsigned long now = millis();
bool wifiConnected = (WiFi.status() == WL_CONNECTED);
if (mode == 0) {
// eyes for eyesDuration
if (now - modeStart < eyesDuration) {
if (now - lastFrame >= 150) {
// animate positions
u8g2.clearBuffer();
drawTopBar(batteryPercent, wifiConnected);
drawEyesContent(posList[posIndex], posList[posIndex], false);
u8g2.sendBuffer();
posIndex = (posIndex + 1) % 8;
lastFrame = now;
}
// occasional blink
if (now - lastBlink > 2000 && (random(0, 100) < 6)) {
u8g2.clearBuffer();
drawTopBar(batteryPercent, wifiConnected);
drawEyesContent(0, 0, true);
u8g2.sendBuffer();
vTaskDelay(pdMS_TO_TICKS(160));
u8g2.clearBuffer();
drawTopBar(batteryPercent, wifiConnected);
drawEyesContent(0, 0, false);
u8g2.sendBuffer();
lastBlink = now;
}
} else {
mode = 1;
modeStart = now;
}
} else {
if (now - modeStart < infoDuration) {
u8g2.clearBuffer();
drawTopBar(batteryPercent, wifiConnected);
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setDrawColor(1);
// Time and location
if (cur.epochTime > 0) {
struct tm ti;
localtime_r(&cur.epochTime, &ti);
char timebuf[9];
strftime(timebuf, sizeof(timebuf), "%H:%M:%S", &ti);
u8g2.drawStr(0, 12, timebuf);
} else {
u8g2.drawStr(0, 12, "Time: --");
}
u8g2.drawStr(50, 12, "Tangerang");
// Date and room
if (cur.epochTime > 0) {
struct tm ti;
localtime_r(&cur.epochTime, &ti);
char datebuf[12];
strftime(datebuf, sizeof(datebuf), "%d %b %Y", &ti);
u8g2.drawStr(0, 24, datebuf);
} else {
u8g2.drawStr(0, 24, "Date: --");
}
// Room
char roombuf[12];
if (!isnan(roomTemp) && !isnan(roomHum)) {
snprintf(roombuf, sizeof(roombuf), "%.0f°C %.0f%%", roomTemp, roomHum);
} else {
strcpy(roombuf, "Room -- --");
}
u8g2.drawStr(70, 24, roombuf);
// Weather
int iconY = 28;
drawWeatherIcon(0, iconY, weatherMain);
if (!isnan(weatherTemp)) {
u8g2.setFont(u8g2_font_7x13B_tr); // Larger for temp
char tempbuf[6];
snprintf(tempbuf, sizeof(tempbuf), "%.0f°", weatherTemp);
u8g2.drawStr(20, 42, tempbuf);
u8g2.setFont(u8g2_font_6x10_tf);
if (!isnan(weatherHigh) && !isnan(weatherLow)) {
char hbuf[8];
snprintf(hbuf, sizeof(hbuf), "H:%.0f°", weatherHigh);
u8g2.drawStr(20, 52, hbuf);
char lbuf[8];
snprintf(lbuf, sizeof(lbuf), "L:%.0f°", weatherLow);
u8g2.drawStr(60, 52, lbuf);
} else {
u8g2.drawStr(20, 52, "H/L --");
}
} else {
u8g2.setFont(u8g2_font_7x13B_tr);
u8g2.drawStr(20, 42, "--°");
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(20, 52, "Weather --");
}
u8g2.sendBuffer();
} else {
mode = 0;
modeStart = now;
}
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
// ---------------- setup & loop ----------------
void setup() {
Serial.begin(115200);
delay(100);
// I2C init for user pins
Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, 20, "Init...");
u8g2.sendBuffer();
delay(600);
// DHT init
dht.begin();
// buzzer pin
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// ADC config
analogReadResolution(12); // 12-bit
// Set attenuation for the chosen pin (attempt to allow up to ~3.3V measured)
#if defined(ARDUINO_ARCH_ESP32)
analogSetPinAttenuation(BAT_ADC_PIN, ADC_11db);
#endif
// 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));
}Loading
xiao-esp32-s3
xiao-esp32-s3