ESP32 OLED Smart Display with Weather & Touch - Bambu3Design – 3D Printing & Design
ESP32 OLED Smart Display Promo

ESP32 OLED Smart Display with Weather & Touch

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <time.h>

// =====================================================
// PINS
// =====================================================

#define OLED_SDA   21
#define OLED_SCL   22
#define TOUCH_PIN  27

#define TOUCH_ACTIVE HIGH

// =====================================================
// OLED
// =====================================================

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(
  U8G2_R0,
  U8X8_PIN_NONE
);

// =====================================================
// CONFIG PORTAL
// =====================================================

const char* AP_NAME = "B3D-Setup";
const char* AP_PASSWORD = "12345678";

WebServer server(80);
Preferences prefs;

// =====================================================
// SAVED SETTINGS
// =====================================================

String wifiSSID = "";
String wifiPassword = "";
String apiKey = "";

String city = "Dublin";
String countryCode = "IE";

const char* timezoneDublin =
  "GMT0IST,M3.5.0/1,M10.5.0";

// =====================================================
// WEATHER
// =====================================================

float temperature = 0.0;
int humidity = 0;

String weatherMain = "";
String weatherDescription = "Loading...";

bool weatherAvailable = false;

unsigned long lastWeatherUpdate = 0;

const unsigned long WEATHER_INTERVAL =
  600000;

// =====================================================
// PAGES
// =====================================================

#define PAGE_FACE     0
#define PAGE_CLOCK    1
#define PAGE_TEMP     2
#define PAGE_WEATHER  3

int currentPage = PAGE_FACE;

// =====================================================
// MOODS
// =====================================================

#define MOOD_NORMAL 0
#define MOOD_HAPPY  1
#define MOOD_SAD    2
#define MOOD_ANGRY  3

int currentMood = MOOD_NORMAL;

// =====================================================
// TOUCH
// =====================================================

bool previousTouch = false;

unsigned long touchStarted = 0;

bool longPressDone = false;

const unsigned long LONG_PRESS_TIME = 900;

// =====================================================
// EYE ANIMATION
// =====================================================

int pupilOffsetX = 0;
int pupilOffsetY = 0;

unsigned long lastEyeMove = 0;

bool blinking = false;

unsigned long blinkStarted = 0;
unsigned long nextBlink = 0;

// =====================================================
// DISPLAY UPDATE
// =====================================================

unsigned long lastScreenUpdate = 0;

// =====================================================
// WIFI RETRY
// =====================================================

unsigned long lastWiFiAttempt = 0;

const unsigned long WIFI_RETRY = 15000;

bool wifiWasConnected = false;

// =====================================================
// CENTER TEXT
// =====================================================

void centerText(
  String text,
  int y,
  const uint8_t* font
) {

  u8g2.setFont(font);

  int width =
    u8g2.getStrWidth(
      text.c_str()
    );

  int x =
    (128 - width) / 2;

  if (x < 0) x = 0;

  u8g2.drawStr(
    x,
    y,
    text.c_str()
  );
}

// =====================================================
// LOAD SETTINGS
// =====================================================

void loadSettings() {

  prefs.begin("b3d", true);

  wifiSSID =
    prefs.getString(
      "ssid",
      ""
    );

  wifiPassword =
    prefs.getString(
      "pass",
      ""
    );

  apiKey =
    prefs.getString(
      "api",
      ""
    );

  city =
    prefs.getString(
      "city",
      "Dublin"
    );

  countryCode =
    prefs.getString(
      "country",
      "IE"
    );

  prefs.end();

  Serial.println("Settings loaded");
}

// =====================================================
// SAVE SETTINGS
// =====================================================

void saveSettings(
  String newSSID,
  String newPassword,
  String newAPI,
  String newCity,
  String newCountry
) {

  prefs.begin("b3d", false);

  prefs.putString(
    "ssid",
    newSSID
  );

  prefs.putString(
    "pass",
    newPassword
  );

  prefs.putString(
    "api",
    newAPI
  );

  prefs.putString(
    "city",
    newCity
  );

  prefs.putString(
    "country",
    newCountry
  );

  prefs.end();
}

// =====================================================
// SETUP WEB PAGE
// =====================================================

void handleRoot() {

  String html = R"rawliteral(
<!DOCTYPE html>
<html>

<head>
<meta name="viewport"
content="width=device-width, initial-scale=1">

<title>B3D Setup</title>

<style>

body{
  font-family:Arial;
  background:#111827;
  color:white;
  padding:25px;
  max-width:420px;
  margin:auto;
}

h1{
  color:#ff7214;
  font-size:38px;
}

p{
  color:#bbb;
}

label{
  display:block;
  margin-top:18px;
  font-size:17px;
}

input{
  width:100%;
  box-sizing:border-box;
  padding:13px;
  margin-top:6px;
  border-radius:8px;
  border:1px solid #555;
  font-size:17px;
}

button{
  width:100%;
  padding:15px;
  margin-top:25px;
  background:#ff7214;
  color:white;
  border:none;
  border-radius:8px;
  font-size:19px;
}

</style>
</head>

<body>

<h1>B3D Setup</h1>

<p>Enter Wi-Fi and Weather settings</p>

<form action="/save" method="POST">

<label>Wi-Fi Name</label>

<input
name="ssid"
value=")rawliteral";

  html += wifiSSID;

  html += R"rawliteral("
placeholder="Wi-Fi name">

<label>Wi-Fi Password</label>

<input
name="pass"
type="password"
placeholder="Wi-Fi password">

<label>OpenWeather API Key</label>

<input
name="api"
value=")rawliteral";

  html += apiKey;

  html += R"rawliteral("
placeholder="OpenWeather API Key">

<label>City</label>

<input
name="city"
value=")rawliteral";

  html += city;

  html += R"rawliteral(">

<label>Country Code</label>

<input
name="country"
value=")rawliteral";

  html += countryCode;

  html += R"rawliteral(">

<button type="submit">
SAVE & CONNECT
</button>

</form>

</body>
</html>
)rawliteral";

  server.send(
    200,
    "text/html",
    html
  );
}

// =====================================================
// SAVE FROM WEB PAGE
// =====================================================

void handleSave() {

  String newSSID =
    server.arg("ssid");

  String newPassword =
    server.arg("pass");

  String newAPI =
    server.arg("api");

  String newCity =
    server.arg("city");

  String newCountry =
    server.arg("country");

  if (
    newSSID.length() == 0
  ) {

    server.send(
      400,
      "text/plain",
      "Wi-Fi name required"
    );

    return;
  }

  // اگر پسورد خالی باشد، قبلی بماند
  if (
    newPassword.length() == 0
  ) {

    newPassword =
      wifiPassword;
  }

  if (
    newCity.length() == 0
  ) {

    newCity = "Dublin";
  }

  if (
    newCountry.length() == 0
  ) {

    newCountry = "IE";
  }

  saveSettings(
    newSSID,
    newPassword,
    newAPI,
    newCity,
    newCountry
  );

  server.send(
    200,
    "text/html",
    "<html><body style='font-family:Arial;background:#111827;color:white;text-align:center;padding-top:60px;'>"
    "<h1 style='color:#ff7214'>Saved!</h1>"
    "<p>ESP32 is restarting...</p>"
    "</body></html>"
  );

  delay(2000);

  ESP.restart();
}

// =====================================================
// START SETUP WIFI
// =====================================================

void startSetupWiFi() {

  WiFi.mode(
    WIFI_AP_STA
  );

  WiFi.setSleep(
    false
  );

  bool ok =
    WiFi.softAP(
      AP_NAME,
      AP_PASSWORD
    );

  if (ok) {

    Serial.println(
      "B3D-Setup started"
    );

    Serial.print(
      "Setup IP: "
    );

    Serial.println(
      WiFi.softAPIP()
    );
  }

  server.on(
    "/",
    HTTP_GET,
    handleRoot
  );

  server.on(
    "/save",
    HTTP_POST,
    handleSave
  );

  server.begin();
}

// =====================================================
// CONNECT HOME WIFI
// =====================================================

void connectHomeWiFi() {

  if (
    wifiSSID.length() == 0
  ) {

    return;
  }

  Serial.print(
    "Connecting to: "
  );

  Serial.println(
    wifiSSID
  );

  WiFi.begin(
    wifiSSID.c_str(),
    wifiPassword.c_str()
  );

  unsigned long start =
    millis();

  while (
    WiFi.status() !=
    WL_CONNECTED &&
    millis() - start <
    15000
  ) {

    server.handleClient();

    delay(250);

    Serial.print(".");
  }

  Serial.println();

  if (
    WiFi.status() ==
    WL_CONNECTED
  ) {

    Serial.println(
      "WiFi Connected!"
    );

    Serial.println(
      WiFi.localIP()
    );
  }

  else {

    Serial.println(
      "Home WiFi failed"
    );
  }
}

// =====================================================
// CLOCK
// =====================================================

void startClock() {

  if (
    WiFi.status() !=
    WL_CONNECTED
  ) {

    return;
  }

  configTzTime(
    timezoneDublin,
    "pool.ntp.org",
    "time.google.com"
  );

  struct tm t;

  getLocalTime(
    &t,
    10000
  );
}

// =====================================================
// WEATHER
// =====================================================

void getWeather() {

  if (
    WiFi.status() !=
    WL_CONNECTED
  ) {

    weatherAvailable =
      false;

    return;
  }

  if (
    apiKey.length() == 0
  ) {

    weatherAvailable =
      false;

    return;
  }

  HTTPClient http;

  String url =
    "http://api.openweathermap.org/data/2.5/weather?q=" +
    city +
    "," +
    countryCode +
    "&appid=" +
    apiKey +
    "&units=metric";

  http.begin(url);

  int code =
    http.GET();

  Serial.print(
    "Weather HTTP: "
  );

  Serial.println(
    code
  );

  if (
    code == 200
  ) {

    String payload =
      http.getString();

    JSONVar data =
      JSON.parse(
        payload
      );

    if (
      JSON.typeof(data) !=
      "undefined"
    ) {

      temperature =
        (double)
        data["main"]["temp"];

      humidity =
        (int)
        data["main"]["humidity"];

      weatherMain =
        (const char*)
        data["weather"][0]["main"];

      weatherDescription =
        (const char*)
        data["weather"][0]["description"];

      weatherAvailable =
        true;
    }
  }

  http.end();

  lastWeatherUpdate =
    millis();
}

// =====================================================
// TOUCH
// =====================================================

void handleTouch() {

  bool touched =
    digitalRead(
      TOUCH_PIN
    ) ==
    TOUCH_ACTIVE;

  if (
    touched &&
    !previousTouch
  ) {

    touchStarted =
      millis();

    longPressDone =
      false;
  }

  // LONG PRESS = CHANGE FACE

  if (
    touched &&
    previousTouch &&
    !longPressDone
  ) {

    if (
      millis() -
      touchStarted >
      LONG_PRESS_TIME
    ) {

      currentMood++;

      if (
        currentMood >
        MOOD_ANGRY
      ) {

        currentMood =
          MOOD_NORMAL;
      }

      currentPage =
        PAGE_FACE;

      longPressDone =
        true;
    }
  }

  // SHORT TAP = CHANGE PAGE

  if (
    !touched &&
    previousTouch
  ) {

    if (
      !longPressDone
    ) {

      currentPage++;

      if (
        currentPage >
        PAGE_WEATHER
      ) {

        currentPage =
          PAGE_FACE;
      }
    }
  }

  previousTouch =
    touched;
}

// =====================================================
// EYE ANIMATION
// =====================================================

void updateEyes() {

  unsigned long now =
    millis();

  if (
    now -
    lastEyeMove >
    1400
  ) {

    lastEyeMove =
      now;

    pupilOffsetX =
      random(
        -5,
        6
      );

    pupilOffsetY =
      random(
        -3,
        4
      );
  }

  if (
    !blinking &&
    now >
    nextBlink
  ) {

    blinking =
      true;

    blinkStarted =
      now;
  }

  if (
    blinking &&
    now -
    blinkStarted >
    130
  ) {

    blinking =
      false;

    nextBlink =
      now +
      random(
        2000,
        5500
      );
  }
}

// =====================================================
// NORMAL FACE
// =====================================================

void drawNormalFace() {

  u8g2.drawRBox(
    18,14,36,34,8
  );

  u8g2.drawRBox(
    74,14,36,34,8
  );

  u8g2.setDrawColor(0);

  u8g2.drawRBox(
    30 + pupilOffsetX,
    24 + pupilOffsetY,
    13,
    16,
    4
  );

  u8g2.drawRBox(
    86 + pupilOffsetX,
    24 + pupilOffsetY,
    13,
    16,
    4
  );

  u8g2.setDrawColor(1);

  u8g2.drawDisc(
    39 + pupilOffsetX,
    27 + pupilOffsetY,
    2
  );

  u8g2.drawDisc(
    95 + pupilOffsetX,
    27 + pupilOffsetY,
    2
  );

  u8g2.drawHLine(
    58,
    56,
    12
  );
}

// =====================================================
// HAPPY FACE
// =====================================================

void drawHappyFace() {

  u8g2.drawLine(
    18,32,28,23
  );

  u8g2.drawLine(
    28,23,38,32
  );

  u8g2.drawLine(
    90,32,100,23
  );

  u8g2.drawLine(
    100,23,110,32
  );

  u8g2.drawLine(
    50,47,56,53
  );

  u8g2.drawLine(
    56,53,64,56
  );

  u8g2.drawLine(
    64,56,72,53
  );

  u8g2.drawLine(
    72,53,78,47
  );
}

// =====================================================
// SAD FACE
// =====================================================

void drawSadFace() {

  u8g2.drawRBox(
    18,18,34,30,8
  );

  u8g2.drawRBox(
    76,18,34,30,8
  );

  u8g2.setDrawColor(0);

  u8g2.drawDisc(
    35,
    31,
    5
  );

  u8g2.drawDisc(
    93,
    31,
    5
  );

  u8g2.setDrawColor(1);

  u8g2.drawLine(
    52,58,58,53
  );

  u8g2.drawLine(
    58,53,64,51
  );

  u8g2.drawLine(
    64,51,70,53
  );

  u8g2.drawLine(
    70,53,76,58
  );
}

// =====================================================
// ANGRY FACE
// =====================================================

void drawAngryFace() {

  u8g2.drawRBox(
    18,20,34,29,6
  );

  u8g2.drawRBox(
    76,20,34,29,6
  );

  u8g2.drawLine(
    16,12,51,22
  );

  u8g2.drawLine(
    77,22,112,12
  );

  u8g2.setDrawColor(0);

  u8g2.drawDisc(
    36,
    35,
    5
  );

  u8g2.drawDisc(
    92,
    35,
    5
  );

  u8g2.setDrawColor(1);

  u8g2.drawHLine(
    54,
    57,
    20
  );
}

// =====================================================
// FACE PAGE
// =====================================================

void drawFacePage() {

  updateEyes();

  if (
    blinking
  ) {

    u8g2.drawRBox(
      18,31,36,4,2
    );

    u8g2.drawRBox(
      74,31,36,4,2
    );

    return;
  }

  if (
    currentMood ==
    MOOD_NORMAL
  ) {

    drawNormalFace();
  }

  else if (
    currentMood ==
    MOOD_HAPPY
  ) {

    drawHappyFace();
  }

  else if (
    currentMood ==
    MOOD_SAD
  ) {

    drawSadFace();
  }

  else {

    drawAngryFace();
  }
}

// =====================================================
// CLOCK PAGE
// =====================================================

void drawClockPage() {

  struct tm t;

  if (
    !getLocalTime(
      &t,
      50
    )
  ) {

    centerText(
      "Syncing...",
      34,
      u8g2_font_6x12_tf
    );

    return;
  }

  char timeText[10];
  char dateText[24];

  strftime(
    timeText,
    sizeof(timeText),
    "%H:%M",
    &t
  );

  strftime(
    dateText,
    sizeof(dateText),
    "%d %b %Y",
    &t
  );

  centerText(
    "DUBLIN",
    10,
    u8g2_font_5x8_tf
  );

  centerText(
    timeText,
    39,
    u8g2_font_logisoso24_tf
  );

  centerText(
    dateText,
    60,
    u8g2_font_6x12_tf
  );
}

// =====================================================
// TEMP PAGE
// =====================================================

void drawTempPage() {

  centerText(
    "DUBLIN",
    11,
    u8g2_font_6x12_tf
  );

  if (
    !weatherAvailable
  ) {

    centerText(
      "Waiting...",
      37,
      u8g2_font_6x12_tf
    );

    return;
  }

  char tempText[16];

  snprintf(
    tempText,
    sizeof(tempText),
    "%.1f C",
    temperature
  );

  centerText(
    tempText,
    39,
    u8g2_font_logisoso20_tf
  );

  char humText[20];

  snprintf(
    humText,
    sizeof(humText),
    "Humidity: %d%%",
    humidity
  );

  centerText(
    humText,
    59,
    u8g2_font_6x12_tf
  );
}

// =====================================================
// WEATHER PAGE
// =====================================================

void drawWeatherPage() {

  centerText(
    "DUBLIN WEATHER",
    11,
    u8g2_font_5x8_tf
  );

  if (
    !weatherAvailable
  ) {

    centerText(
      "Weather unavailable",
      37,
      u8g2_font_5x8_tf
    );

    return;
  }

  centerText(
    weatherMain,
    35,
    u8g2_font_7x14B_tf
  );

  centerText(
    weatherDescription,
    59,
    u8g2_font_6x12_tf
  );
}

// =====================================================
// DRAW PAGE
// =====================================================

void drawCurrentPage() {

  u8g2.clearBuffer();

  if (
    currentPage ==
    PAGE_FACE
  ) {

    drawFacePage();
  }

  else if (
    currentPage ==
    PAGE_CLOCK
  ) {

    drawClockPage();
  }

  else if (
    currentPage ==
    PAGE_TEMP
  ) {

    drawTempPage();
  }

  else {

    drawWeatherPage();
  }

  u8g2.sendBuffer();
}

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

void setup() {

  Serial.begin(115200);

  delay(1000);

  pinMode(
    TOUCH_PIN,
    INPUT
  );

  Wire.begin(
    OLED_SDA,
    OLED_SCL
  );

  u8g2.begin();

  u8g2.setContrast(255);

  randomSeed(micros());

  loadSettings();

  // --------------------------------
  // ALWAYS START SETUP WIFI
  // --------------------------------

  startSetupWiFi();

  // --------------------------------
  // OLED SETUP INFO
  // --------------------------------

  u8g2.clearBuffer();

  centerText(
    "B3D SETUP",
    12,
    u8g2_font_6x12_tf
  );

  u8g2.setFont(
    u8g2_font_5x8_tf
  );

  u8g2.drawStr(
    0,
    29,
    "WiFi: B3D-Setup"
  );

  u8g2.drawStr(
    0,
    42,
    "Pass: 12345678"
  );

  u8g2.drawStr(
    0,
    56,
    "IP: 192.168.4.1"
  );

  u8g2.sendBuffer();

  delay(1500);

  // --------------------------------
  // HOME WIFI
  // --------------------------------

  connectHomeWiFi();

  if (
    WiFi.status() ==
    WL_CONNECTED
  ) {

    startClock();

    getWeather();

    wifiWasConnected =
      true;
  }

  nextBlink =
    millis() +
    3000;

  currentPage =
    PAGE_FACE;
}

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

void loop() {

  // صفحه تنظیمات همیشه فعال
  server.handleClient();

  unsigned long now =
    millis();

  handleTouch();

  // --------------------------------
  // WiFi reconnect
  // --------------------------------

  bool connected =
    WiFi.status() ==
    WL_CONNECTED;

  if (
    !connected &&
    wifiSSID.length() > 0 &&
    now -
    lastWiFiAttempt >
    WIFI_RETRY
  ) {

    lastWiFiAttempt =
      now;

    WiFi.begin(
      wifiSSID.c_str(),
      wifiPassword.c_str()
    );
  }

  // تازه وصل شده
  if (
    connected &&
    !wifiWasConnected
  ) {

    wifiWasConnected =
      true;

    startClock();

    getWeather();
  }

  if (
    !connected
  ) {

    wifiWasConnected =
      false;
  }

  // Weather refresh

  if (
    connected &&
    now -
    lastWeatherUpdate >
    WEATHER_INTERVAL
  ) {

    getWeather();
  }

  // Display refresh

  if (
    now -
    lastScreenUpdate >
    50
  ) {

    lastScreenUpdate =
      now;

    drawCurrentPage();
  }
}

Scroll to Top