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.
This is a very small gadget with many features that shows the accurate readings of weather and also if you take outside it shows the real live uv index of your area.
Also this shows some funny emojis when in idle mode and there are other many features of this weather station.
Made from 3d printed enclosure and powered by esp32 c3 super mini this is a tiny device that will fit your pocket!.
I have given all the necessary materials and source codes to build this project.
Working video of this project is here
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
You can simply use my design or make your own!
I have taken reference of the standard component which is esp32 c3 board here and the uv sensor module.
The oled space was for ssd1306 and most of them use the same or have the same.

After you design the frame export the model to slicer, i will be using ultimaker cura slicer to slice these.
Don’t have slicer? get the ultimaker cura for free.
Use your favorite color pla filament to print the model, I will use red for the base frame and for the lid i will use blue pla filament.
This won’t take much longer to print the model as we have smaller components for printing here.
Meanwhile while the model is being 3d printed you can proceed to making the electronics circuit part.
Arduino Code for UV Weather Station with Live UV Meter
This is the program for our weather station.
Here you have to replace your wifi credentials instead of mine.
You can get the weather api from any weather web application which shows the key of your place.
Now you can select the type of board here it is the 3sp32 c3 super mini and proper com port and proceed with the upload.
#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);
}
You can either build the circuit using the below circuit diagram or first upload the code and later make the circuit.
If you face any error while uploading the arduino program check for the libraries and it should work fine.
Diy mini weather station circuit diagram
This is the circuit diagram to make live uv meter with weather station using esp32 c3 super mini.
Since there is only 2 components involved building the circuit connections is very easy.
I will start by connecting the OLED module first to the esp32 board and then the uv sensor.

OLED has 4 pins, Gnd and Vcc are connect to the 3v and the gnd pins of the esp32 c3 super mini board.
Sck and Sda is connect to 9 and 8 pin of esp32 c3.
UV sensor module has 3 pins and the power pins are connect to 3v and the ground of esp32 board.
The signal pin is connect to pin 0 on esp32-c3 board and now we are done with the connections.
Assembling Electronics Inside 3D Printed Frame
This is the working circuit that you will get after you upload the program.
Now after the connections, Connect the 3sp32 board to usb power supply with a type c cable and the OLED screen gives a welcome message.
If you are able to see this screen as shown above the wiring is correct.

Not happy with this messy circuit?
You can even shrink this circuit by using the PCB and the custom PCB you can get from JLCPCB
No matter what projects you are working on and what is your requirement they have all the PCB types for any type of projects.

No need to install anything simply use their EasyEDA tool embedded inside their site to design PCB.
Visit their site to check all the services they offer and make use of it.

How To Use
Using this is very fun and definitely you will enjoy seeing your project come to life.
Simply connect the board to USB power supply and the OLED screen shows message of Wi-Fi being connected.
The wifi will be connect automatically and you will see it on the OLED module.
It will take few seconds to fetch the details from server and then the screen shows all the details

For the UV readings just take this device on outdoors and the screen starts showing the UV index of your place.
In the mean time the emojis come up which makes this look like a cool desktop gadget.
This was all about making this Mini weather station at home, If you have any questions ask in the comments.
You may also like my previous project on smart arduino garage door



