OLED Animation Maker OLED Maker

Arduino OLED Weather Station with DHT22 — Temp, Humidity & Icons

A desk weather station is the first OLED project that feels useful every day — not another scrolling hello. Temperature and humidity on a 128×64 SSD1306, with a humidity bar and a tiny sun or cloud icon, is enough to make the display earn its keep.

This tutorial uses a DHT22 (AM2302) for the readings, Adafruit SSD1306 + GFX for the UI, and the Adafruit DHT library for sampling. You get Uno and ESP32 pin maps, vector icons drawn with circles and lines (no bitmaps required), a NaN strategy that keeps the last good values on screen, and a complete sketch you can paste into the Arduino IDE.

What you will build

  • Large temperature readout (°C) plus humidity percent on one 128×64 I2C OLED
  • Horizontal humidity bar scaled 0–100%
  • Sun and cloud icons drawn with Adafruit GFX primitives based on simple thresholds
  • Non-blocking DHT reads every ~2 s (DHT22’s minimum interval)
  • NaN / failed-read handling that retains last good temp and humidity
  • A “sensor error” flag line so you know when the bus or sensor is unhappy

Parts and wiring

  • Arduino Uno / Nano or ESP32 (DevKit-style board)
  • I2C SSD1306 128×64 OLED
  • DHT22 module (or bare AM2302 with a 4.7k–10k pull-up on data)
  • Jumper wires; optional breadboard
PartArduino Uno / NanoESP32 (common)
OLED VCC / GND5V / GND3.3V / GND
OLED SDA / SCLA4 / A5GPIO21 / GPIO22
DHT22 VCC / GND5V / GND (or 3.3V if module allows)3.3V / GND
DHT22 DATAD2 (changeable)GPIO4 (changeable)
DHT data pull-upOften on-module; else 4.7k to VCCSame

Confirm the OLED on 0x3C (or 0x3D) with an I2C scanner before blaming the DHT. For DHT22, leave at least 2 seconds between reads — sampling faster returns garbage or NaN. If the screen stays black, use the blank-screen checklist first.

Libraries

Install from Library Manager:

  • Adafruit GFX Library
  • Adafruit SSD1306
  • DHT sensor library by Adafruit (pulls in Adafruit Unified Sensor)
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C
#define DHTPIN 2          // GPIO4 on ESP32 in the full sketch variants
#define DHTTYPE DHT22

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DHT dht(DHTPIN, DHTTYPE);

On ESP32, change DHTPIN to 4 (or whatever GPIO you wired). Do not use strapping pins for the sensor if you can avoid them.

Layout on a 128×64 panel

Mentally divide the screen:

  1. Top-left ~80×40 — big temperature digits
  2. Top-right — weather icon (sun or cloud)
  3. Mid band — “H: 45%” text
  4. Bottom — humidity bar with outline

Fixed pixel positions beat relative “niceness” on OLED — you want numbers that do not jump when humidity ticks from 9% to 10%. Use monospace-ish placement: right-pad or clear a fixed region before redrawing digits.

void drawTempLarge(float t) {
  display.setTextSize(3);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 4);
  display.print(t, 1);
  display.setTextSize(1);
  display.setCursor(90, 8);
  display.print("C");
}

Humidity bar

A filled rectangle scaled to 0–100% is easier to glance at than raw digits. Clamp inputs so a bad reading never draws a bar wider than the outline.

void drawHumidityBar(float h) {
  const int x = 0, y = 52, w = 128, hBar = 10;
  display.drawRect(x, y, w, hBar, SSD1306_WHITE);
  int fill = (int)constrain(h, 0.0f, 100.0f) * (w - 2) / 100;
  if (fill > 0) {
    display.fillRect(x + 1, y + 1, fill, hBar - 2, SSD1306_WHITE);
  }
}

Optional refinement: invert the fill (black hole growing left-to-right on a white bar) for a denser look, or draw a vertical tick every 25% so “50%” is obvious without reading text.

Sun and cloud icons with GFX

Skip bitmaps for workshop stations. A sun is a filled circle plus eight short rays; a cloud is three overlapping circles with a flat rectangle base. Choose the icon from humidity (or later temperature trend) so the screen feels alive:

  • Humidity < 55% → sun
  • Humidity ≥ 55% → cloud
void drawSun(int cx, int cy) {
  display.fillCircle(cx, cy, 6, SSD1306_WHITE);
  for (int a = 0; a < 360; a += 45) {
    float rad = a * 0.0174533f;
    int x0 = cx + (int)(8 * cos(rad));
    int y0 = cy + (int)(8 * sin(rad));
    int x1 = cx + (int)(11 * cos(rad));
    int y1 = cy + (int)(11 * sin(rad));
    display.drawLine(x0, y0, x1, y1, SSD1306_WHITE);
  }
}

void drawCloud(int cx, int cy) {
  display.fillCircle(cx - 6, cy, 5, SSD1306_WHITE);
  display.fillCircle(cx + 2, cy - 2, 6, SSD1306_WHITE);
  display.fillCircle(cx + 8, cy + 1, 5, SSD1306_WHITE);
  display.fillRect(cx - 10, cy, 20, 6, SSD1306_WHITE);
}

Place icons around (112, 16) so they clear the big temperature. If you hate floating-point in the ray loop, hardcode eight drawLine pairs — clearer for beginners and avoids cmath on some AVR builds.

Want custom season icons later? Draw them in OLED Animation Maker, export PROGMEM bitmaps, and swap drawBitmap for these helpers while leaving the layout and DHT logic alone.

NaN handling and last good values

DHT22 fails occasionally: short wires get noise, people touch the data line, or you poll too soon after boot. Adafruit’s readTemperature() / readHumidity() return NaN on failure. Never print those raw — you get a blank or broken UI.

Keep last-good floats, update them only when both readings are valid, and flag the UI when a sample fails so you are not silently stuck on stale numbers forever.

float lastTemp = 0.0f;
float lastHum  = 0.0f;
bool  haveGood = false;
bool  lastReadOk = false;

bool sampleDht() {
  float h = dht.readHumidity();
  float t = dht.readTemperature(); // Celsius
  if (isnan(h) || isnan(t)) {
    lastReadOk = false;
    return false;
  }
  lastHum = h;
  lastTemp = t;
  haveGood = true;
  lastReadOk = true;
  return true;
}

On the first boot before any good sample, show -- and leave the bar empty. After the first success, redraw with last-good even when a later poll fails — but show a tiny “!” or “ERR” so you notice a disconnected sensor instead of believing yesterday’s air forever.

Timing: read vs redraw

Poll DHT every 2000–2500 ms. Redraw the OLED at the same cadence, or slightly faster for a soft blink of status text only. Do not call display() at 30 Hz for a weather UI — it burns power and buys nothing. See the companion article on stretching battery life if this station will run from a USB bank or LiPo for days.

unsigned long nextSampleAt = 0;
const unsigned long SAMPLE_MS = 2000;

void loop() {
  unsigned long now = millis();
  if (now >= nextSampleAt) {
    nextSampleAt = now + SAMPLE_MS;
    sampleDht();
    redrawUi();
  }
}

Full sketch (copy-paste)

Uno / Nano default: DHT on D2, OLED at 0x3C. For ESP32, set DHTPIN to 4 and power the OLED from 3.3V. Libraries: Adafruit GFX, Adafruit SSD1306, Adafruit DHT.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <math.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C

// Uno/Nano: 2 · ESP32: use 4 (and 3.3V for OLED)
#define DHTPIN 2
#define DHTTYPE DHT22

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DHT dht(DHTPIN, DHTTYPE);

float lastTemp = 0.0f;
float lastHum = 0.0f;
bool haveGood = false;
bool lastReadOk = false;

unsigned long nextSampleAt = 0;
const unsigned long SAMPLE_MS = 2200UL;

void drawSun(int cx, int cy) {
  display.fillCircle(cx, cy, 6, SSD1306_WHITE);
  // 8 rays without float/cos — beginner-friendly
  display.drawLine(cx, cy - 11, cx, cy - 8, SSD1306_WHITE);
  display.drawLine(cx, cy + 8, cx, cy + 11, SSD1306_WHITE);
  display.drawLine(cx - 11, cy, cx - 8, cy, SSD1306_WHITE);
  display.drawLine(cx + 8, cy, cx + 11, cy, SSD1306_WHITE);
  display.drawLine(cx - 8, cy - 8, cx - 6, cy - 6, SSD1306_WHITE);
  display.drawLine(cx + 6, cy - 6, cx + 8, cy - 8, SSD1306_WHITE);
  display.drawLine(cx - 8, cy + 8, cx - 6, cy + 6, SSD1306_WHITE);
  display.drawLine(cx + 6, cy + 6, cx + 8, cy + 8, SSD1306_WHITE);
}

void drawCloud(int cx, int cy) {
  display.fillCircle(cx - 6, cy, 5, SSD1306_WHITE);
  display.fillCircle(cx + 2, cy - 2, 6, SSD1306_WHITE);
  display.fillCircle(cx + 8, cy + 1, 5, SSD1306_WHITE);
  display.fillRect(cx - 10, cy, 20, 6, SSD1306_WHITE);
}

void drawHumidityBar(float h) {
  const int x = 0, y = 52, w = 128, barH = 10;
  display.drawRect(x, y, w, barH, SSD1306_WHITE);
  int fill = (int)(constrain(h, 0.0f, 100.0f) * (w - 2) / 100.0f);
  if (fill > 0) {
    display.fillRect(x + 1, y + 1, fill, barH - 2, SSD1306_WHITE);
  }
}

bool sampleDht() {
  float h = dht.readHumidity();
  float t = dht.readTemperature();
  if (isnan(h) || isnan(t)) {
    lastReadOk = false;
    return false;
  }
  lastHum = h;
  lastTemp = t;
  haveGood = true;
  lastReadOk = true;
  return true;
}

void redrawUi() {
  display.clearDisplay();

  if (!haveGood) {
    display.setTextSize(2);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(8, 20);
    display.print("--.- C");
    display.setTextSize(1);
    display.setCursor(8, 44);
    display.print("Waiting for DHT22...");
    display.display();
    return;
  }

  // large temperature
  display.setTextSize(3);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 4);
  display.print(lastTemp, 1);
  display.setTextSize(1);
  display.setCursor(92, 6);
  display.print((char)247); // degree-ish; fallback below if font lacks it
  display.setCursor(100, 8);
  display.print("C");

  // humidity text
  display.setCursor(0, 36);
  display.print("H: ");
  display.print(lastHum, 0);
  display.print("%");

  // status
  if (!lastReadOk) {
    display.setCursor(70, 36);
    display.print("ERR");
  }

  // icon from humidity
  if (lastHum < 55.0f) drawSun(112, 18);
  else drawCloud(110, 18);

  drawHumidityBar(lastHum);
  display.display();
}

void setup() {
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    for (;;);
  }
  dht.begin();
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 24);
  display.print("Weather boot...");
  display.display();
  delay(1500); // DHT22 needs warm-up after power
  nextSampleAt = millis();
}

void loop() {
  unsigned long now = millis();
  if (now >= nextSampleAt) {
    nextSampleAt = now + SAMPLE_MS;
    sampleDht();
    redrawUi();
  }
}

The degree character is font-dependent. If you see garbage beside °C, drop the (char)247 line and print a plain "C" only — same clarity on a monochrome panel.

ESP32 pin variant

Same sketch with two defines:

#define DHTPIN 4
// Power OLED from 3.3V. Wire DHT VCC to 3.3V on most ESP32 modules.

If Wi-Fi is enabled later for a “live weather” dashboard, keep DHT sampling on its timer and never block loop() with long delay() chains — same discipline as animation sketches.

Calibration and placement tips

  • Do not bury the DHT under the OLED ribbon heat from a sealed box; vent it or leave a window.
  • Self-heating: keep the sensor a few centimeters from regulators and ESP32 chips.
  • Outdoor boxes need dew protection; condensation kills exposed PCBs and hangs humidity near 99%.
  • Compare against a known thermometer once; offset with a constant if your module runs 1–2°C high.

Extending the UI

Add a second “page” toggled by a button: min/max today, heat-index estimate, or a tiny sparkline of the last 60 humidity samples in a ring buffer. For multi-page OLED UX patterns, the menu/navigation article shows non-blocking button handling that pairs cleanly with this station.

Battery-powered stations should dim contrast, slow the sample interval to 10–30 s, and use DISPLAYOFF between updates — covered step-by-step in the power-consumption guide linked below.

Troubleshooting FAQ

Always NaN / ERR after boot

Check DATA pin number, shared GND, and pull-up. Wait ≥2 s between reads. Swap to another GPIO — some Nano clones mark pins oddly. Confirm the sensor is DHT22, not DHT11 (wrong DHTTYPE yields nonsense).

OLED black but Serial prints temp fine

Address wrong (0x3C vs 0x3D), 5V-only module on ESP32 3.3V logic with weak pull-ups, or insufficient power. Run the blank-screen checklist.

Humidity stuck at 99% or 0%

Wet / sealed enclosure, or a dying sensor. Power-cycle after drying. Bare DHT22 without pull-up will also sit in limbo — add 4.7k.

Numbers flicker or tear

Clear then draw the whole frame once, then single display(). Do not call clearDisplay between temperature and humidity. Slow SAMPLE_MS if I2C wiring is long.

Uno runs out of SRAM with extra widgets

Avoid String, keep icons as vectors, and skip dual full-screen bitmaps. See the Uno memory limits article before stacking Wi-Fi + OLED buffers.

SH1106 panel looks shifted

Controller mismatch — use an SH1106 init or X offset. Details in SH1106 vs SSD1306.

Related

Design custom weather icons

Draw sun, rain, and moon as tiny sprites in the free tool, export PROGMEM arrays, and drop them into this station layout.

Open oledanimationmaker.com →