SCIENCE PROJECTS

Diy mini weather station with esp32-c3 super mini

Hello readers this project is about Diy mini weather station with esp32-c3 super mini that also shows real time uv readings from the sunlight.

Materials required to build this project

ESP32 C3 super mini board

UV Sensor Module

Jumper Cables

OLED Module

Arduino IDE

3D Printer

PLA Filament

Slicing Software

Tinkercad Design of Frame

I will be using Tinkercad to design the frame for this project, and it is very easy

Arduino Code for UV Weather Station with Live UV Meter

This is the program for our weather station

#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <esp_wifi.h>
#include <time.h>

// ============================================================
//  CONFIGURATION
// ============================================================
const char* ssid        = "yourid";
const char* password    = "123123123";
const char* apiKey      = "9a78053936753yadfd737780df2792be(yourapikey)";
const char* city        = "Bengaluru";
const char* countryCode = "IN";

#define UV_SENSOR_PIN  0
#define SDA_PIN        8
#define SCL_PIN        9
#define SCREEN_WIDTH   128
#define SCREEN_HEIGHT  64
#define OLED_RESET     -1
#define YELLOW_BAR_H   16   // top 16px = blue on your display
#define WHITE_TOP      16   // alias — first row of the lower white zone
#define WHITE_H        48   // usable lower rows (64-16)

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
WebServer server(80);

// ── Data ──────────────────────────────────────────────────
float  uvIndex     = 0;
float  temperature = 0;
float  humidity    = 0;
float  pressure    = 0;
float  windSpeed   = 0;
float  feelsLike   = 0;
String weatherDesc = "---";

// ── Display modes ─────────────────────────────────────────
enum DisplayMode { UV_MODE, WEATHER_MODE, EYES_MODE };
DisplayMode currentMode  = UV_MODE;
bool        wifiOK        = false;
bool        serverStarted = false;
uint8_t     animTick      = 0;

unsigned long lastWeatherUpdate  = 0;
unsigned long lastModeSwitch     = 0;
unsigned long lastOLEDRefresh    = 0;
unsigned long lastWifiCheck      = 0;

const unsigned long UV_DURATION      =  7000UL;
const unsigned long WEATHER_DURATION =  7000UL;
const unsigned long EYES_DURATION    =  5000UL;
const unsigned long WEATHER_INTERVAL = 300000UL;
const unsigned long OLED_REFRESH     =    80UL;
const unsigned long WIFI_CHECK_MS    = 20000UL;

// ── Eye animation state ───────────────────────────────────
struct Eyes {
  float lx, ly, rx, ry;
  float tlx, tly, trx, try_;
  float blinkL, blinkR;
  bool  blinking;
  unsigned long nextBlink;
  uint8_t expression;
  unsigned long expressionEnd;
} eyes;

// ── UV helpers ────────────────────────────────────────────
const char* uvLabel(float uv) {
  if (uv < 3)  return "LOW";
  if (uv < 6)  return "MODERATE";
  if (uv < 8)  return "HIGH";
  if (uv < 11) return "V.HIGH";
  return "EXTREME";
}

// ============================================================
//  OLED HELPERS
// ============================================================
void oledHeader(const char* left, const char* right = nullptr) {
  display.fillRect(0, 0, SCREEN_WIDTH, YELLOW_BAR_H, SSD1306_WHITE);
  display.setTextColor(SSD1306_BLACK);
  display.setTextSize(1);
  if (!right) {
    int x = (SCREEN_WIDTH - (int)strlen(left) * 6) / 2;
    display.setCursor(max(0, x), 4);
    display.print(left);
  } else {
    display.setCursor(3, 4);
    display.print(left);
    display.setCursor(SCREEN_WIDTH - (int)strlen(right) * 6 - 2, 4);
    display.print(right);
  }
  display.setTextColor(SSD1306_WHITE);
}

void hLine(int y) {
  display.drawFastHLine(0, y, SCREEN_WIDTH, SSD1306_WHITE);
}

void drawSignalBars(int x, int y, int rssi) {
  int level = (rssi >= -50) ? 4 : (rssi >= -65) ? 3 : (rssi >= -75) ? 2 : 1;
  for (int i = 0; i < 4; i++) {
    int bh = (i + 1) * 2;
    int bx = x + i * 4;
    int by = y + (8 - bh);
    if (i < level) display.fillRect(bx, by, 3, bh, SSD1306_WHITE);
    else           display.drawRect(bx, by, 3, bh, SSD1306_WHITE);
  }
}

// Draw arc from angle a1 to a2 around cx,cy at radius r
void drawArc(int cx, int cy, int r, float a1, float a2, uint16_t col) {
  for (float a = a1; a <= a2; a += 0.06f) {
    int px = cx + (int)(r * cos(a));
    int py = cy + (int)(r * sin(a));
    if (px >= 0 && px < 128 && py >= 0 && py < 64)
      display.drawPixel(px, py, col);
  }
}

// ============================================================
//  SCREEN: UV INDEX
// ============================================================
void drawUVScreen() {
  struct tm ti; char tbuf[6] = "--:--";
  if (getLocalTime(&ti)) strftime(tbuf, sizeof(tbuf), "%H:%M", &ti);
  oledHeader("SOLAR  UV", tbuf);

  // Arc: semicircle, centre at (64, 52), radius 30
  int arcCX = 64, arcCY = 52, arcR = 30;

  // Background arc
  drawArc(arcCX, arcCY, arcR,     PI, 2 * PI, SSD1306_WHITE);
  drawArc(arcCX, arcCY, arcR - 1, PI, 2 * PI, SSD1306_WHITE);

  // Sun position along arc
  float uvRatio  = constrain(uvIndex / 11.0f, 0.0f, 1.0f);
  float sunAngle = PI + uvRatio * PI;
  int   sunX     = arcCX + (int)(arcR * cos(sunAngle));
  int   sunY     = arcCY + (int)(arcR * sin(sunAngle));

  // Filled arc up to sun position
  for (float a = PI; a <= sunAngle; a += 0.05f) {
    for (int dr = -1; dr <= 1; dr++) {
      int px = arcCX + (int)((arcR + dr) * cos(a));
      int py = arcCY + (int)((arcR + dr) * sin(a));
      if (px >= 0 && px < 128 && py >= WHITE_TOP && py < 64)
        display.drawPixel(px, py, SSD1306_WHITE);
    }
  }

  // Sun circle at current position
  if (sunY >= WHITE_TOP) {
    display.fillCircle(sunX, sunY, 4, SSD1306_BLACK);
    display.drawCircle(sunX, sunY, 4, SSD1306_WHITE);
    display.fillCircle(sunX, sunY, 2, SSD1306_WHITE);
  }

  // Horizon line
  display.drawFastHLine(arcCX - arcR - 2, arcCY, arcR * 2 + 4, SSD1306_WHITE);

  // UV value centred above arc — textSize 2
  String uvStr = String(uvIndex, 1);
  int tw = uvStr.length() * 12;
  display.setTextSize(2);
  display.setCursor((SCREEN_WIDTH - tw) / 2, 18);
  display.print(uvStr);

  // Risk pill
  const char* lbl = uvLabel(uvIndex);
  int pw  = strlen(lbl) * 6 + 8;
  int px2 = (SCREEN_WIDTH - pw) / 2;
  display.fillRoundRect(px2, 54, pw, 10, 3, SSD1306_WHITE);
  display.setTextColor(SSD1306_BLACK);
  display.setTextSize(1);
  display.setCursor(px2 + 4, 56);
  display.print(lbl);
  display.setTextColor(SSD1306_WHITE);

  // Signal bars bottom-right
  drawSignalBars(112, 54, WiFi.RSSI());
}

// ============================================================
//  SCREEN: WEATHER
// ============================================================
void drawWeatherScreen() {
  struct tm ti; char tbuf[9] = "--:--:--";
  if (getLocalTime(&ti)) strftime(tbuf, sizeof(tbuf), "%H:%M:%S", &ti);
  oledHeader(city, tbuf);

  // Temperature
  display.setTextSize(2);
  display.setCursor(2, 17);
  display.print(String(temperature, 1));
  display.setTextSize(1);
  display.print("\xF8""C");

  // Feels like right
  char fl[12]; snprintf(fl, sizeof(fl), "FL:%.0f\xF8", feelsLike);
  display.setTextSize(1);
  display.setCursor(SCREEN_WIDTH - (int)strlen(fl) * 6 - 1, 18);
  display.print(fl);

  // Humidity right 2nd line
  char hh[8]; snprintf(hh, sizeof(hh), "H:%.0f%%", humidity);
  display.setCursor(SCREEN_WIDTH - (int)strlen(hh) * 6 - 1, 27);
  display.print(hh);

  hLine(29);

  // Description
  String desc = weatherDesc; desc.toUpperCase();
  if (desc.length() > 21) desc = desc.substring(0, 20) + "~";
  display.setTextSize(1);
  display.setCursor(2, 31);
  display.print(desc);

  hLine(39);

  // Wind + Pressure
  display.setCursor(2, 41);
  display.printf("W:%.1fm/s", windSpeed);
  display.setCursor(64, 41);
  display.printf("P:%dhPa", (int)pressure);

  hLine(49);

  // UV mini arc
  int mx = 12, my = 62, mr = 7;
  drawArc(mx, my, mr, PI, 2 * PI, SSD1306_WHITE);
  float uvR2 = constrain(uvIndex / 11.0f, 0, 1);
  float sa2  = PI + uvR2 * PI;
  for (float a = PI; a <= sa2; a += 0.12f)
    display.drawPixel(mx + (int)(mr * cos(a)), my + (int)(mr * sin(a)), SSD1306_WHITE);

  // Sun dot on mini arc
  int sdx = mx + (int)(mr * cos(sa2));
  int sdy = my + (int)(mr * sin(sa2));
  if (sdy >= WHITE_TOP) display.fillCircle(sdx, sdy, 1, SSD1306_WHITE);

  display.setCursor(22, 52);
  display.print(String(uvIndex, 1));
  display.setCursor(22, 59);
  display.print(uvLabel(uvIndex));

  drawSignalBars(110, 53, WiFi.RSSI());
}

// ============================================================
//  SCREEN: ANIMATED EYES
// ============================================================
void drawEye(int cx, int cy, int ew, int eh,
             float blinkAmt, float pox, float poy,
             bool happy, bool surprised, bool sleepy) {
  int hw = ew / 2;
  int hh = surprised ? eh / 2 + 2 : sleepy ? max(2, (int)(eh / 2 * 0.5f)) : eh / 2;

  display.drawRoundRect(cx - hw, cy - hh, ew, hh * 2, min(hw, hh) - 1, SSD1306_WHITE);

  if (blinkAmt > 0.01f) {
    int lidH = (int)(blinkAmt * hh * 2);
    display.fillRect(cx - hw - 1, cy - hh, ew + 2, lidH, SSD1306_BLACK);
    display.drawRoundRect(cx - hw, cy - hh, ew, hh * 2, min(hw, hh) - 1, SSD1306_WHITE);
    if (!happy) display.drawFastHLine(cx - hw, cy - hh + lidH, ew, SSD1306_WHITE);
  }

  if (happy && blinkAmt < 0.3f) {
    for (int dx = -hw + 1; dx < hw; dx++) {
      float t = (float)dx / hw;
      int dy  = (int)(t * t * hh * 0.7f);
      int px2 = cx + dx, py2 = cy + dy;
      if (py2 < cy + hh && py2 >= cy - hh)
        display.drawPixel(px2, py2, SSD1306_WHITE);
    }
  }

  if (blinkAmt < 0.8f) {
    int px2 = constrain(cx + (int)(pox * (hw - 3)), cx - hw + 2, cx + hw - 2);
    int py2 = constrain(cy + (int)(poy * (hh - 3)), cy - hh + 2, cy + hh - 2);
    int pr  = surprised ? 4 : 3;
    display.fillCircle(px2, py2, pr, SSD1306_WHITE);
    display.drawPixel(px2 - 1, py2 - 1, SSD1306_BLACK);
  }
}

void pickExpression() {
  static unsigned long lastPick = 0;
  if (millis() - lastPick < 2000) return;
  lastPick = millis();
  if (uvIndex >= 8) {
    eyes.expression = 5;
  } else {
    uint8_t r = random(0, 10);
    if      (r < 3) eyes.expression = 1;
    else if (r < 5) eyes.expression = 2;
    else if (r < 6) eyes.expression = 3;
    else if (r < 8) eyes.expression = 4;
    else            eyes.expression = 0;
  }
  eyes.expressionEnd = millis() + random(1500, 4000);
}

void updatePupils() {
  float spd = 0.12f;
  eyes.lx += (eyes.tlx - eyes.lx) * spd;
  eyes.ly += (eyes.tly - eyes.ly) * spd;
  eyes.rx += (eyes.trx - eyes.rx) * spd;
  eyes.ry += (eyes.try_ - eyes.ry) * spd;

  static unsigned long nextMove = 0;
  if (millis() > nextMove) {
    nextMove = millis() + random(600, 2000);
    float tx = ((float)random(-80, 81)) / 100.0f;
    float ty = ((float)random(-60, 61)) / 100.0f;
    eyes.tlx = tx; eyes.tly = ty;
    if (random(0, 8) == 0) {
      eyes.trx = -tx; eyes.try_ = ty;
    } else {
      eyes.trx = tx + ((float)random(-15, 16)) / 100.0f;
      eyes.try_ = ty;
    }
  }
}

void updateBlink() {
  static unsigned long blinkStart = 0;
  static bool inBlink = false;
  static float blinkPhase = 0;

  if (!inBlink && millis() > eyes.nextBlink) {
    inBlink    = true;
    blinkStart = millis();
    eyes.blinking  = (eyes.expression == 2);
    eyes.nextBlink = millis() + random(2000, 5000);
  }

  if (inBlink) {
    unsigned long elapsed = millis() - blinkStart;
    if      (elapsed < 80)  blinkPhase = elapsed / 80.0f;
    else if (elapsed < 160) blinkPhase = 1.0f - (elapsed - 80) / 80.0f;
    else { blinkPhase = 0; inBlink = false; }

    if (eyes.blinking) { eyes.blinkL = blinkPhase; eyes.blinkR = 0; }
    else               { eyes.blinkL = blinkPhase; eyes.blinkR = blinkPhase; }
  }
}

void drawEyesScreen() {
  const char* exprNames[] = {
    "( . _ . )", "( ^ o ^ )", "( ^_- )", "( o  O )", "( -_- )", "( >_< )"
  };
  oledHeader(exprNames[eyes.expression]);

  updatePupils();
  updateBlink();
  pickExpression();

  bool happy     = (eyes.expression == 1);
  bool wink      = (eyes.expression == 2);
  bool surprised = (eyes.expression == 3);
  bool sleepy    = (eyes.expression == 4);
  bool uvWarn    = (eyes.expression == 5);

  int eyeW  = surprised ? 22 : 20;
  int eyeH  = surprised ? 14 : sleepy ? 7 : 12;
  int eyeCY = 37;
  int lEyeCX = 38, rEyeCX = 90;

  drawEye(lEyeCX, eyeCY, eyeW, eyeH,
          wink ? 1.0f : eyes.blinkL,
          eyes.lx, eyes.ly, happy, surprised, sleepy);

  drawEye(rEyeCX, eyeCY, eyeW, eyeH,
          eyes.blinkR,
          eyes.rx, eyes.ry, happy, surprised, sleepy);

  int mouthCX = 64, mouthY = 54;

  if (happy) {
    drawArc(mouthCX, mouthY, 10, 0.2f, PI - 0.2f, SSD1306_WHITE);
    drawArc(mouthCX, mouthY,  9, 0.2f, PI - 0.2f, SSD1306_WHITE);
  } else if (uvWarn) {
    drawArc(mouthCX, mouthY + 6, 8, PI + 0.2f, 2 * PI - 0.2f, SSD1306_WHITE);
    drawArc(mouthCX, mouthY + 6, 7, PI + 0.2f, 2 * PI - 0.2f, SSD1306_WHITE);
    display.fillCircle(100, 30, 2, SSD1306_WHITE);
    display.drawPixel(100, 27, SSD1306_WHITE);
  } else if (surprised) {
    display.drawCircle(mouthCX, mouthY, 5, SSD1306_WHITE);
  } else if (sleepy) {
    display.drawFastHLine(mouthCX - 7, mouthY, 14, SSD1306_WHITE);
    display.setTextSize(1);
    display.setCursor(100, 26); display.print("z");
    display.setCursor(106, 21); display.print("z");
    display.setCursor(112, 17); display.print("Z");
  } else {
    drawArc(mouthCX, mouthY, 6, 0.3f, PI - 0.3f, SSD1306_WHITE);
  }

  if (uvWarn) {
    display.setTextSize(1);
    display.setCursor(2, 56);
    display.printf("UV:%.1f WEAR SPF!", uvIndex);
  }
}

// ============================================================
//  SPLASH
// ============================================================
void splashScreen() {
  display.clearDisplay();
  oledHeader("ROBOHUB  V3.0");

  display.setTextSize(1);
  display.setCursor(2, 19); display.print("UV WEATHER STATION");
  display.setCursor(18, 29); display.print("ESP32-C3 MINI");

  display.drawRoundRect(28, 38, 16, 10, 4, SSD1306_WHITE);
  display.fillCircle(36, 43, 3, SSD1306_WHITE);
  display.drawRoundRect(84, 38, 16, 10, 4, SSD1306_WHITE);
  display.fillCircle(92, 43, 3, SSD1306_WHITE);
  drawArc(64, 56, 7, 0.2f, PI - 0.2f, SSD1306_WHITE);
  drawArc(64, 56, 6, 0.2f, PI - 0.2f, SSD1306_WHITE);

  display.display();
  delay(1500);

  display.fillRect(0, 36, 128, 28, SSD1306_BLACK);
  display.drawRect(4, 40, 120, 6, SSD1306_WHITE);
  display.display();

  for (int i = 0; i <= 100; i++) {
    int f = (int)(i * 1.16f);
    display.fillRect(5, 41, f, 4, SSD1306_WHITE);
    display.fillRect(0, 50, 128, 12, SSD1306_BLACK);
    display.setCursor(2, 52);
    if      (i < 25) display.print("Booting...");
    else if (i < 55) display.print("WiFi init...");
    else if (i < 85) display.print("Connecting...");
    else             display.print("Almost ready!");
    display.setCursor(112, 52); display.printf("%d%%", i);
    if (i % 3 == 0) display.display();
    delay(10);
  }
  display.display();
  delay(300);
}

// ============================================================
//  CONNECTING SCREEN
// ============================================================
void drawConnectingScreen(int attempt, wl_status_t status) {
  display.clearDisplay();
  oledHeader("CONNECTING...");
  display.setTextSize(1);

  display.setCursor(2, 18); display.print("SSID: "); display.print(ssid);

  display.setCursor(2, 28);
  switch (status) {
    case WL_DISCONNECTED:   display.print("Associating..."); break;
    case WL_NO_SSID_AVAIL:  display.print("SSID not found"); break;
    case WL_CONNECT_FAILED: display.print("Auth failed!");   break;
    default:                display.printf("Status: %d", status); break;
  }

  display.setCursor(2, 38); display.printf("Try %d / 40", attempt);

  display.drawRect(4, 47, 120, 5, SSD1306_WHITE);
  int fill = (int)(attempt / 40.0f * 118);
  if (fill > 0) display.fillRect(5, 48, fill, 3, SSD1306_WHITE);

  // Nervous eyes
  display.drawRoundRect(28, 54, 14, 8, 3, SSD1306_WHITE);
  display.fillCircle(35, 58, 2, SSD1306_WHITE);
  display.drawRoundRect(86, 54, 14, 8, 3, SSD1306_WHITE);
  display.fillCircle(93, 58, 2, SSD1306_WHITE);

  display.display();
}

// ============================================================
//  CONNECTED SCREEN
// ============================================================
void showConnectedScreen() {
  display.clearDisplay();
  oledHeader("WIFI  CONNECTED");
  display.setTextSize(1);

  display.setCursor(2, 19); display.print("IP: "); display.print(WiFi.localIP().toString());
  hLine(28);
  display.setCursor(2, 31); display.printf("RSSI: %d dBm", WiFi.RSSI());
  drawSignalBars(100, 30, WiFi.RSSI());
  hLine(40);
  display.setCursor(2, 43); display.printf("CH: %d   2.4GHz", WiFi.channel());
  hLine(52);

  // Happy eyes
  display.drawRoundRect(24, 54, 16, 9, 4, SSD1306_WHITE);
  display.fillCircle(32, 58, 3, SSD1306_WHITE);
  display.drawRoundRect(88, 54, 16, 9, 4, SSD1306_WHITE);
  display.fillCircle(96, 58, 3, SSD1306_WHITE);
  drawArc(64, 60, 5, 0.2f, PI - 0.2f, SSD1306_WHITE);

  display.display();
  delay(2500);
}

// ============================================================
//  FAIL SCREEN
// ============================================================
void showFailScreen() {
  display.clearDisplay();
  oledHeader("CONNECT FAILED");
  display.setTextSize(1);
  display.setCursor(2, 19); display.print("- Hotspot: 2.4GHz");
  display.setCursor(2, 28); display.print("- SSID/pass correct?");
  display.setCursor(2, 37); display.print("- Check serial log");
  hLine(46);
  display.setCursor(2, 49); display.print("Retrying in 20s...");

  // Sad eyes
  display.drawRoundRect(24, 54, 16, 9, 4, SSD1306_WHITE);
  display.fillCircle(32, 58, 3, SSD1306_WHITE);
  display.drawRoundRect(88, 54, 16, 9, 4, SSD1306_WHITE);
  display.fillCircle(96, 58, 3, SSD1306_WHITE);
  drawArc(64, 63, 5, PI + 0.2f, 2 * PI - 0.2f, SSD1306_WHITE);

  display.display();
}

// ============================================================
//  NO WIFI SCREEN
// ============================================================
void drawNoWiFiScreen() {
  oledHeader("NO CONNECTION");
  display.setTextSize(1);
  const char* frames[] = {"Searching .  ", "Searching .. ", "Searching ..."};
  display.setCursor(14, 20); display.print(frames[(animTick / 4) % 3]);
  display.setCursor(2, 31);  display.print("SSID: "); display.print(ssid);
  display.setCursor(2, 42);  display.printf("UV: %.1f (offline)", uvIndex);
  hLine(51);
  display.drawRoundRect(24, 53, 16, 9, 4, SSD1306_WHITE);
  display.fillCircle(32, 57, 3, SSD1306_WHITE);
  display.drawRoundRect(88, 53, 16, 9, 4, SSD1306_WHITE);
  display.fillCircle(96, 57, 3, SSD1306_WHITE);
}

// ============================================================
//  OLED DRIVER
// ============================================================
void updateOLED() {
  display.clearDisplay();

  if (!wifiOK) {
    drawNoWiFiScreen();
    display.display();
    return;
  }

  unsigned long now     = millis();
  unsigned long elapsed = now - lastModeSwitch;

  bool advance = false;
  if (currentMode == UV_MODE      && elapsed >= UV_DURATION)      advance = true;
  if (currentMode == WEATHER_MODE && elapsed >= WEATHER_DURATION) advance = true;
  if (currentMode == EYES_MODE    && elapsed >= EYES_DURATION)    advance = true;

  if (advance) {
    if      (currentMode == UV_MODE)      currentMode = WEATHER_MODE;
    else if (currentMode == WEATHER_MODE) currentMode = EYES_MODE;
    else                                  currentMode = UV_MODE;
    lastModeSwitch = now;
  }

  if      (currentMode == UV_MODE)      drawUVScreen();
  else if (currentMode == WEATHER_MODE) drawWeatherScreen();
  else                                  drawEyesScreen();

  display.display();
}

// ============================================================
//  WIFI CONNECT
// ============================================================
bool connectWiFi() {
  Serial.println("\n[WiFi] Starting ESP32-C3 Mini sequence");
  WiFi.disconnect(true, true);
  delay(500);
  WiFi.mode(WIFI_OFF); delay(200);
  WiFi.mode(WIFI_STA); delay(500);
  esp_wifi_set_max_tx_power(40);
  WiFi.setSleep(false);
  WiFi.setAutoReconnect(false);
  WiFi.begin(ssid, password);

  for (int i = 0; i < 40; i++) {
    delay(500);
    wl_status_t s = WiFi.status();
    Serial.printf("[WiFi] attempt %d  status=%d\n", i + 1, (int)s);
    drawConnectingScreen(i + 1, s);
    if (s == WL_CONNECTED) {
      Serial.printf("[WiFi] Connected! IP: %s\n", WiFi.localIP().toString().c_str());
      return true;
    }
    if (s == WL_NO_SSID_AVAIL && i > 6) {
      WiFi.disconnect(); delay(300); WiFi.begin(ssid, password);
    }
    if (i == 20) {
      WiFi.disconnect(true); delay(500);
      WiFi.mode(WIFI_STA);   delay(300);
      esp_wifi_set_max_tx_power(40);
      WiFi.setSleep(false);
      WiFi.begin(ssid, password);
    }
  }
  return false;
}

// ============================================================
//  SETUP
// ============================================================
void setup() {
  Serial.begin(115200);
  delay(1000);

  Wire.begin(SDA_PIN, SCL_PIN);
  pinMode(UV_SENSOR_PIN, INPUT);
  analogReadResolution(12);

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 fail")); while (true);
  }
  display.cp437(true);
  randomSeed(analogRead(1));

  memset(&eyes, 0, sizeof(eyes));
  eyes.nextBlink  = millis() + 1500;
  eyes.expression = 1;

  splashScreen();

  wifiOK = connectWiFi();

  if (wifiOK) {
    configTime(5.5 * 3600, 0, "pool.ntp.org", "time.nist.gov");
    server.on("/",     handleRoot);
    server.on("/data", handleData);
    server.begin();
    serverStarted = true;
    showConnectedScreen();
    fetchWeatherData();
    lastWeatherUpdate = millis();
  } else {
    showFailScreen();
  }

  currentMode     = UV_MODE;
  lastModeSwitch  = millis();
  lastOLEDRefresh = millis();
  lastWifiCheck   = millis();
}

// ============================================================
//  LOOP
// ============================================================
void loop() {
  if (millis() - lastWifiCheck >= WIFI_CHECK_MS) {
    lastWifiCheck = millis();
    if (WiFi.status() != WL_CONNECTED) {
      wifiOK = false;
      if (connectWiFi()) {
        wifiOK = true;
        if (!serverStarted) {
          server.on("/", handleRoot); server.on("/data", handleData);
          server.begin(); serverStarted = true;
        }
        fetchWeatherData(); lastWeatherUpdate = millis();
      }
    } else { wifiOK = true; }
  }

  if (wifiOK && serverStarted) server.handleClient();

  int raw = analogRead(UV_SENSOR_PIN);
  uvIndex = max(0.0f, (raw / 4095.0f) * 3.3f * 10.0f);

  if (wifiOK && millis() - lastWeatherUpdate >= WEATHER_INTERVAL) {
    fetchWeatherData(); lastWeatherUpdate = millis();
  }

  if (millis() - lastOLEDRefresh >= OLED_REFRESH) {
    animTick++;
    updateOLED();
    lastOLEDRefresh = millis();
  }
}

// ============================================================
//  WEATHER FETCH
// ============================================================
void fetchWeatherData() {
  if (WiFi.status() != WL_CONNECTED) return;
  HTTPClient http;
  http.setTimeout(8000);
  String url = "http://api.openweathermap.org/data/2.5/weather?q="
               + String(city) + "," + String(countryCode)
               + "&units=metric&appid=" + String(apiKey);
  http.begin(url);
  if (http.GET() == 200) {
    JsonDocument doc;
    if (!deserializeJson(doc, http.getString())) {
      temperature = doc["main"]["temp"]       | 0.0f;
      feelsLike   = doc["main"]["feels_like"] | 0.0f;
      humidity    = doc["main"]["humidity"]   | 0.0f;
      pressure    = doc["main"]["pressure"]   | 0.0f;
      windSpeed   = doc["wind"]["speed"]      | 0.0f;
      weatherDesc = doc["weather"][0]["description"].as<String>();
    }
  }
  http.end();
}

// ============================================================
//  WEB — /data
// ============================================================
void handleData() {
  struct tm ti; char buf[20] = "--:--:--";
  if (getLocalTime(&ti)) strftime(buf, sizeof(buf), "%H:%M:%S", &ti);
  JsonDocument doc;
  doc["uv"]   = serialized(String(uvIndex, 1));
  doc["temp"] = serialized(String(temperature, 1));
  doc["feel"] = serialized(String(feelsLike, 1));
  doc["hum"]  = serialized(String(humidity, 0));
  doc["wind"] = serialized(String(windSpeed, 1));
  doc["pres"] = serialized(String(pressure, 0));
  doc["desc"] = weatherDesc;
  doc["time"] = buf;
  doc["ip"]   = WiFi.localIP().toString();
  doc["risk"] = uvLabel(uvIndex);
  doc["rssi"] = WiFi.RSSI();
  String j; serializeJson(doc, j);
  server.send(200, "application/json", j);
}

// ============================================================
//  WEB — /
// ============================================================
void handleRoot() {
  const char* page = R"RAWHTML(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>RoboHub UV Station</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
  --bg:#060d14;--s1:#0d1f30;
  --gold:#ffd632;--white:#e8f0f8;
  --dim:rgba(232,240,248,0.45);--dimmer:rgba(232,240,248,0.2);
  --safe:#3dffa0;--border:rgba(255,220,50,0.1);
}
body{
  background:var(--bg);color:var(--white);
  font-family:'DM Sans',sans-serif;min-height:100vh;
  display:flex;flex-direction:column;align-items:center;
  padding:28px 14px 60px;
  background-image:
    radial-gradient(ellipse 80% 50% at 50% -10%,rgba(255,214,50,0.07) 0%,transparent 60%),
    repeating-linear-gradient(90deg,rgba(255,255,255,0.012) 0,rgba(255,255,255,0.012) 1px,transparent 1px,transparent 52px),
    repeating-linear-gradient(0deg,rgba(255,255,255,0.012) 0,rgba(255,255,255,0.012) 1px,transparent 1px,transparent 52px);
}
header{width:100%;max-width:900px;display:flex;justify-content:space-between;align-items:center;margin-bottom:32px;padding-bottom:16px;border-bottom:1px solid var(--border)}
.logo{font-family:'Orbitron',monospace;font-size:12px;font-weight:900;letter-spacing:5px;color:var(--gold)}
.logo span{opacity:.4;font-weight:400}
.hdr-right{display:flex;align-items:center;gap:20px}
.rssi-wrap{display:flex;align-items:flex-end;gap:3px;height:16px}
.rssi-wrap b{width:4px;border-radius:1px;background:var(--dimmer);transition:background .4s}
.rssi-wrap b:nth-child(1){height:5px}
.rssi-wrap b:nth-child(2){height:8px}
.rssi-wrap b:nth-child(3){height:11px}
.rssi-wrap b:nth-child(4){height:16px}
.rssi-wrap b.on{background:var(--safe)}
.live{display:flex;align-items:center;gap:8px;font-size:11px;letter-spacing:3px;color:var(--dim)}
.live::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--safe);box-shadow:0 0 10px var(--safe);animation:pulse 1.8s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}
.grid{width:100%;max-width:900px;display:grid;grid-template-columns:repeat(auto-fit,minmax(270px,1fr));gap:18px}
.card{background:var(--s1);border:1px solid var(--border);border-radius:22px;padding:26px 24px;position:relative;overflow:hidden;transition:transform .3s,box-shadow .3s}
.card:hover{transform:translateY(-4px);box-shadow:0 24px 52px rgba(0,0,0,.55)}
.card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--gold),transparent);opacity:.4}
.lbl{font-family:'Orbitron',monospace;font-size:10px;font-weight:700;letter-spacing:3px;color:var(--dimmer);margin-bottom:18px;text-transform:uppercase}
.sun-wrap{position:relative;height:160px;display:flex;align-items:center;justify-content:center}
.sun-svg{width:100%;height:160px}
.uv-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-66%);text-align:center}
.uv-big{font-family:'Orbitron',monospace;font-size:64px;font-weight:900;line-height:1;transition:color .6s}
.uv-unit{font-family:'Orbitron',monospace;font-size:12px;color:var(--dimmer);letter-spacing:3px;margin-top:2px}
.risk-pill{display:inline-block;margin-top:14px;padding:7px 20px;border-radius:100px;font-family:'Orbitron',monospace;font-size:11px;font-weight:700;letter-spacing:2px;border:1px solid;transition:all .5s}
.temp-num{font-family:'Orbitron',monospace;font-size:62px;font-weight:900;line-height:1;color:var(--white);letter-spacing:-2px}
.temp-num sup{font-size:22px;vertical-align:super;color:var(--dimmer)}
.w-desc{font-size:13px;color:var(--dim);text-transform:capitalize;margin-top:6px;letter-spacing:1px}
.fl-row{display:flex;gap:8px;align-items:center;margin-top:12px}
.fl-pill{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:5px 12px;font-size:12px;color:var(--dim)}
.stat-list{display:flex;flex-direction:column;gap:11px;margin-top:4px}
.stat{display:flex;justify-content:space-between;align-items:center;padding:11px 14px;background:rgba(255,255,255,.04);border-radius:12px;border:1px solid rgba(255,255,255,.055)}
.sn{font-size:11px;letter-spacing:2px;color:var(--dim);text-transform:uppercase}
.sv{font-family:'Orbitron',monospace;font-size:14px;font-weight:700;color:var(--white)}
.footer{grid-column:1/-1;display:flex;justify-content:space-between;align-items:center;padding-top:6px;font-size:11px;color:var(--dimmer);letter-spacing:1px;flex-wrap:wrap;gap:6px}
.footer strong{color:var(--gold)}
@media(max-width:500px){.uv-big{font-size:48px}.temp-num{font-size:46px}}
</style>
</head>
<body>
<header>
  <div class="logo">RoboHub <span>/ UV Station</span></div>
  <div class="hdr-right">
    <div class="rssi-wrap" id="rssiBar"><b></b><b></b><b></b><b></b></div>
    <div class="live">LIVE</div>
  </div>
</header>
<div class="grid">

<div class="card">
  <div class="lbl">Solar UV Index</div>
  <div class="sun-wrap">
    <svg class="sun-svg" id="sunSvg" viewBox="0 0 260 160" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <linearGradient id="uvGrad" x1="0%" y1="0%" x2="100%" y2="0%">
          <stop offset="0%"   stop-color="#3dffa0"/>
          <stop offset="45%"  stop-color="#ffd632"/>
          <stop offset="75%"  stop-color="#ff8c00"/>
          <stop offset="100%" stop-color="#ff4f4f"/>
        </linearGradient>
      </defs>
      <path d="M20,140 A110,110 0 0,1 240,140" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="6" stroke-linecap="round"/>
      <path id="arcFill" d="M20,140 A110,110 0 0,1 240,140" fill="none" stroke="url(#uvGrad)" stroke-width="6" stroke-linecap="round" stroke-dasharray="345" stroke-dashoffset="345"/>
      <circle id="sunDot"  cx="130" cy="30" r="14" fill="none" stroke="#ffd632" stroke-width="2"/>
      <circle id="sunCore" cx="130" cy="30" r="7"  fill="#ffd632"/>
      <g id="sunRays">
        <line x1="130" y1="10" x2="130" y2="4"   stroke="#ffd632" stroke-width="2" stroke-linecap="round" opacity=".7"/>
        <line x1="130" y1="50" x2="130" y2="56"  stroke="#ffd632" stroke-width="2" stroke-linecap="round" opacity=".7"/>
        <line x1="110" y1="30" x2="104" y2="30"  stroke="#ffd632" stroke-width="2" stroke-linecap="round" opacity=".7"/>
        <line x1="150" y1="30" x2="156" y2="30"  stroke="#ffd632" stroke-width="2" stroke-linecap="round" opacity=".7"/>
        <line x1="116" y1="16" x2="111" y2="11"  stroke="#ffd632" stroke-width="2" stroke-linecap="round" opacity=".5"/>
        <line x1="144" y1="44" x2="149" y2="49"  stroke="#ffd632" stroke-width="2" stroke-linecap="round" opacity=".5"/>
        <line x1="116" y1="44" x2="111" y2="49"  stroke="#ffd632" stroke-width="2" stroke-linecap="round" opacity=".5"/>
        <line x1="144" y1="16" x2="149" y2="11"  stroke="#ffd632" stroke-width="2" stroke-linecap="round" opacity=".5"/>
      </g>
      <text x="16"  y="156" fill="rgba(255,255,255,.3)" font-size="10" font-family="monospace" text-anchor="middle">0</text>
      <text x="72"  y="126" fill="rgba(255,255,255,.3)" font-size="10" font-family="monospace" text-anchor="middle">3</text>
      <text x="130" y="114" fill="rgba(255,255,255,.3)" font-size="10" font-family="monospace" text-anchor="middle">6</text>
      <text x="188" y="126" fill="rgba(255,255,255,.3)" font-size="10" font-family="monospace" text-anchor="middle">8</text>
      <text x="244" y="156" fill="rgba(255,255,255,.3)" font-size="10" font-family="monospace" text-anchor="middle">11</text>
    </svg>
    <div class="uv-center">
      <div class="uv-big" id="uvNum">0.0</div>
      <div class="uv-unit">UV INDEX</div>
    </div>
  </div>
  <div style="text-align:center">
    <div class="risk-pill" id="riskPill">CALCULATING</div>
  </div>
</div>

<div class="card">
  <div class="lbl">Temperature</div>
  <div class="temp-num" id="temp">--<sup>°C</sup></div>
  <div class="w-desc" id="desc">connecting...</div>
  <div class="fl-row">
    <div class="fl-pill">Feels like <strong id="feel" style="color:var(--white)">--°C</strong></div>
    <div class="fl-pill">H: <strong id="hum" style="color:var(--white)">--%</strong></div>
  </div>
  <div class="stat-list" style="margin-top:18px">
    <div class="stat"><span class="sn">Pressure</span><span class="sv" id="pres">-- hPa</span></div>
    <div class="stat"><span class="sn">Wind</span><span class="sv" id="wind">-- m/s</span></div>
  </div>
</div>

<div class="card">
  <div class="lbl">System</div>
  <div class="stat-list">
    <div class="stat"><span class="sn">Signal</span><span class="sv" id="rssiVal">-- dBm</span></div>
    <div class="stat"><span class="sn">Device IP</span><span class="sv" id="ip" style="font-size:11px">---</span></div>
    <div class="stat"><span class="sn">Last Sync</span><span class="sv" id="ts" style="font-size:11px">---</span></div>
  </div>
</div>

<div class="footer">
  <span>UV WEATHER STATION  ·  ESP32-C3 Mini + SSD1306 + OWM</span>
  <span>REFRESH <strong>2s</strong></span>
</div>
</div>

<script>
const ARC_LEN=345;
const RC={
  LOW:      {bg:'rgba(61,255,160,.13)', border:'rgba(61,255,160,.4)',  text:'#3dffa0'},
  MODERATE: {bg:'rgba(255,214,50,.13)', border:'rgba(255,214,50,.4)',  text:'#ffd632'},
  HIGH:     {bg:'rgba(255,140,0,.15)',  border:'rgba(255,140,0,.45)',  text:'#ff8c00'},
  'V.HIGH': {bg:'rgba(255,100,0,.15)',  border:'rgba(255,100,0,.45)',  text:'#ff6432'},
  EXTREME:  {bg:'rgba(255,79,79,.15)',  border:'rgba(255,79,79,.45)',  text:'#ff4f4f'},
};

let rayAngle=0, lastUV=-1;
const sunLines=document.querySelectorAll('#sunRays line');
const basePts=Array.from(sunLines).map(l=>({
  x1:+l.getAttribute('x1'),y1:+l.getAttribute('y1'),
  x2:+l.getAttribute('x2'),y2:+l.getAttribute('y2')
}));

function uvToPos(uv){
  const ratio=Math.min(uv/11,1);
  const angle=Math.PI*(1-ratio);
  return{x:130+110*Math.cos(angle), y:140-110*Math.sin(angle)};
}

function animFrame(){
  rayAngle+=0.4;
  const pos=uvToPos(lastUV<0?0:lastUV);
  const dx=pos.x-130, dy=pos.y-30;
  const cos=Math.cos(rayAngle*Math.PI/180);
  const sin=Math.sin(rayAngle*Math.PI/180);
  sunLines.forEach((l,i)=>{
    const b=basePts[i];
    const rx1=(b.x1-130)*cos-(b.y1-30)*sin+130+dx;
    const ry1=(b.x1-130)*sin+(b.y1-30)*cos+30+dy;
    const rx2=(b.x2-130)*cos-(b.y2-30)*sin+130+dx;
    const ry2=(b.x2-130)*sin+(b.y2-30)*cos+30+dy;
    l.setAttribute('x1',rx1.toFixed(1));l.setAttribute('y1',ry1.toFixed(1));
    l.setAttribute('x2',rx2.toFixed(1));l.setAttribute('y2',ry2.toFixed(1));
  });
  requestAnimationFrame(animFrame);
}
animFrame();

function setRSSI(r){
  const lvl=r>=-50?4:r>=-60?3:r>=-70?2:1;
  document.querySelectorAll('#rssiBar b').forEach((b,i)=>b.classList.toggle('on',i<lvl));
  document.getElementById('rssiVal').textContent=r+' dBm';
}

function update(){
  fetch('/data').then(r=>r.json()).then(d=>{
    const uv=parseFloat(d.uv);
    lastUV=uv;

    document.getElementById('uvNum').textContent=d.uv;
    const c=RC[d.risk]||RC.LOW;
    document.getElementById('uvNum').style.color=c.text;

    const pill=document.getElementById('riskPill');
    pill.textContent=d.risk;
    pill.style.background=c.bg;
    pill.style.borderColor=c.border;
    pill.style.color=c.text;

    document.getElementById('arcFill').style.strokeDashoffset=Math.max(0,ARC_LEN*(1-uv/11));

    const pos=uvToPos(uv);
    document.getElementById('sunDot').setAttribute('cx',pos.x.toFixed(1));
    document.getElementById('sunDot').setAttribute('cy',pos.y.toFixed(1));
    document.getElementById('sunCore').setAttribute('cx',pos.x.toFixed(1));
    document.getElementById('sunCore').setAttribute('cy',pos.y.toFixed(1));

    document.getElementById('temp').innerHTML=d.temp+'<sup>°C</sup>';
    document.getElementById('desc').textContent=d.desc;
    document.getElementById('feel').textContent=d.feel+'°C';
    document.getElementById('hum').textContent=d.hum+'%';
    document.getElementById('wind').textContent=d.wind+' m/s';
    document.getElementById('pres').textContent=d.pres+' hPa';
    document.getElementById('ts').textContent=d.time;
    document.getElementById('ip').textContent=d.ip;
    if(d.rssi)setRSSI(parseInt(d.rssi));
  }).catch(()=>{});
}
setInterval(update,2000);
update();
</script>
</body>
</html>
)RAWHTML";
  server.send(200,"text/html",page);
}

Diy mini weather station circuit diagram

This is the circuit diagram to make live uv meter with weather station using esp32 c3 super mini.

diy mini weather station circuit diagram

Assembling Electronics Inside 3D Printed Frame

This is the working circuit that you will get after you upload the program.

Not happy with my circuit?

Get a custom PCB from JLCPCB they have all the PCB services for any type of projects that you could possibly imagine, Use their EasyEDA tool to design PCB.

uv meter and weather station with esp32 c3 super mini

If you are able to see this screen the wiring is correct.

Visit their site to take advantage of all their services.

Im very happy with their services and they offer fast shipping, First time? Visit them to avail great coupons and offers.

How To Use

Using this is very fun and definitely you will enjoy seeing your project come to life

make mini uv meter at home with weather display

Just connect to any usb power supply and the other end of USB cable to esp32 c3 board and you will see OLED display welcome message.

Jeevan

ADMIN

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button