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
| Part | Arduino Uno / Nano | ESP32 (common) |
|---|---|---|
| OLED VCC / GND | 5V / GND | 3.3V / GND |
| OLED SDA / SCL | A4 / A5 | GPIO21 / GPIO22 |
| DHT22 VCC / GND | 5V / GND (or 3.3V if module allows) | 3.3V / GND |
| DHT22 DATA | D2 (changeable) | GPIO4 (changeable) |
| DHT data pull-up | Often on-module; else 4.7k to VCC | Same |
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:
- Top-left ~80×40 — big temperature digits
- Top-right — weather icon (sun or cloud)
- Mid band — “H: 45%” text
- 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
- OLED power consumption & battery life
- SSD1306 I2C getting started
- Blank screen checklist
- Gauges and progress bars
- Menus and button navigation
- GFX shapes: circle, rect, line
- Battery voltage on OLED
- Uno memory limits
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 →