🔗Goal
Build a compact weather station that reads temperature, humidity, and barometric pressure from a BME280 sensor and displays all three values on a small OLED screen. This project runs entirely offline — no WiFi needed. Just plug in the ESP32 and you have a live, updating weather display.
Here is how the system works at a high level:
graph LR
A[BME280 Sensor] -->|I2C| B[ESP32]
B -->|I2C| C[OLED SSD1306]Both the BME280 and the OLED display use the I2C protocol, so they share the same two data lines (SDA and SCL). This keeps wiring simple — only four connections per device.
🔗Prerequisites
You will need the following components:
| Component | Qty | Notes | Buy |
|---|---|---|---|
| ESP32 dev board | 1 | AliExpress | Amazon.de .co.uk .com | |
| BME280 sensor module | 1 | I2C breakout board (not BMP280 — that one lacks humidity) | AliExpress | Amazon.de .co.uk .com |
| SSD1306 OLED display (0.96") | 1 | I2C interface | AliExpress | Amazon.de .co.uk .com |
| Breadboard | 1 | AliExpress | Amazon.de .co.uk .com | |
| Jumper wires | ~8 | Male-to-male and male-to-female | AliExpress | Amazon.de .co.uk .com |
Links marked Amazon/AliExpress are affiliate links. We may earn a small commission at no extra cost to you.
You will also need the following Arduino libraries. Install them via Sketch > Include Library > Manage Libraries:
| Library | Author | Purpose |
|---|---|---|
| Adafruit BME280 Library | Adafruit | BME280 sensor driver |
| Adafruit Unified Sensor | Adafruit | Dependency for the BME280 library |
| Adafruit SSD1306 | Adafruit | OLED display driver |
| Adafruit GFX Library | Adafruit | Graphics primitives (text, shapes) |
See our BME280 guide for more details on the sensor, including altitude calculation and I2C address configuration.
See our OLED SSD1306 guide for more on display setup, custom fonts, and drawing graphics.
🔗Tutorial
🔗Step 1: Wiring
Both the BME280 and the SSD1306 OLED connect to the same I2C bus. On most ESP32 boards, the default I2C pins are GPIO 21 (SDA) and GPIO 22 (SCL).
| Component | Pin | ESP32 Pin |
|---|---|---|
| BME280 | VIN / VCC | 3.3V |
| BME280 | GND | GND |
| BME280 | SDA | GPIO 21 |
| BME280 | SCL | GPIO 22 |
| SSD1306 OLED | VCC | 3.3V |
| SSD1306 OLED | GND | GND |
| SSD1306 OLED | SDA | GPIO 21 |
| SSD1306 OLED | SCL | GPIO 22 |
Both devices share the same SDA and SCL lines — this is normal for I2C. Each device has a unique address (BME280 is typically
0x76or0x77, and the SSD1306 is typically0x3C).
Pin labels and GPIO numbers vary between ESP32 boards. Always check your board's pinout diagram and datasheet.
🔗Step 2: Verify I2C addresses
If either device is not detected, the most common reason is an I2C address mismatch. Most BME280 modules default to 0x76, but some use 0x77 depending on how the SDO pin is wired on the breakout board. You can use the I2C scanner sketch from the Arduino examples to confirm the addresses.
🔗Step 3: Upload the code
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
// ----- OLED configuration -----
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // No reset pin
#define OLED_ADDRESS 0x3C
// ----- BME280 configuration -----
#define BME280_ADDRESS 0x76 // Change to 0x77 if your module uses that address
// ----- Objects -----
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_BME280 bme;
void setup() {
Serial.begin(115200);
// Initialize I2C (default SDA=21, SCL=22 on most ESP32 boards)
Wire.begin();
// Initialize OLED
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
Serial.println("ERROR: SSD1306 OLED not found. Check wiring and address.");
while (true) { delay(1000); }
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.display();
Serial.println("OLED initialized.");
// Initialize BME280
if (!bme.begin(BME280_ADDRESS)) {
Serial.println("ERROR: BME280 not found. Check wiring and address.");
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("BME280 not found!");
display.println("Check wiring.");
display.display();
while (true) { delay(1000); }
}
Serial.println("BME280 initialized.");
}
void displayWeather(float temp, float humidity, float pressure) {
display.clearDisplay();
// Row 1: Temperature
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Temp:");
display.setTextSize(2);
display.setCursor(0, 10);
display.print(temp, 1);
display.setTextSize(1);
display.print(" ");
display.print((char)247); // Degree symbol
display.print("C");
// Row 2: Humidity
display.setTextSize(1);
display.setCursor(0, 28);
display.print("Humidity:");
display.setTextSize(2);
display.setCursor(0, 38);
display.print(humidity, 1);
display.setTextSize(1);
display.print(" %");
// Row 3: Pressure (bottom of screen)
display.setTextSize(1);
display.setCursor(0, 56);
display.print("Pressure: ");
display.print(pressure, 1);
display.print(" hPa");
display.display();
}
void loop() {
float temperature = bme.readTemperature(); // Celsius
float humidity = bme.readHumidity(); // Percent
float pressure = bme.readPressure() / 100.0F; // Pa to hPa
// Print to Serial Monitor
Serial.print("Temperature: ");
Serial.print(temperature, 1);
Serial.print(" C | Humidity: ");
Serial.print(humidity, 1);
Serial.print(" % | Pressure: ");
Serial.print(pressure, 1);
Serial.println(" hPa");
// Update the OLED
displayWeather(temperature, humidity, pressure);
delay(2000); // Update every 2 seconds
}🔗Step 4: Read the display
After uploading, the OLED should show three rows of information:
- Temperature in large text (degrees Celsius)
- Humidity in large text (percent)
- Pressure in smaller text at the bottom (hectopascals)
The display updates every 2 seconds. You can also open the Serial Monitor at 115200 baud to see the same values printed as text.
🔗Step 5: Understanding the readings
The BME280 measures barometric (atmospheric) pressure. At sea level, standard pressure is $1013.25\,\text{hPa}$. Your local pressure will vary with altitude and weather conditions.
You can estimate altitude from the pressure reading using the barometric formula:
$$h = 44330 \times \left(1 - \left(\frac{P}{P_0}\right)^{0.1903}\right)$$
where $P$ is the measured pressure and $P_0 = 1013.25\,\text{hPa}$ is the reference sea-level pressure. The Adafruit BME280 library includes a readAltitude() function that does this calculation for you.
🔗Next Steps
This project works entirely offline, but you can extend it with WiFi capabilities:
- Web dashboard: Add a web server (like the Room Temperature Monitor) to view readings from your phone
- MQTT logging: Publish readings to an MQTT broker for long-term data logging (see the MQTT Temperature Logger)
- Light sensor: Add a BH1750 light sensor to the I2C bus for a lux reading — see our BH1750 guide
🔗Common Issues and Solutions
| Problem | Cause | Fix |
|---|---|---|
| OLED shows nothing | Wrong I2C address or wiring issue | Run an I2C scanner sketch. Most SSD1306 modules use 0x3C, but some use 0x3D |
| "BME280 not found" in Serial Monitor | Wrong I2C address | Try changing BME280_ADDRESS from 0x76 to 0x77 |
| Temperature reads a few degrees too high | BME280 self-heating | The BME280 generates a small amount of heat during operation. Allow 1-2 minutes for it to stabilize. Keep it away from other heat sources |
| Humidity reads 100% constantly | Sensor damaged by condensation or water | The BME280 is not waterproof. If it was exposed to water, it may need time to dry out or may be permanently damaged |
| Pressure reading seems wrong | The reading is correct — local pressure differs from 1013.25 hPa | Atmospheric pressure varies with altitude and weather. Use a known local reference to compare |
| Display text is garbled or overlapping | Screen dimensions mismatch | Ensure SCREEN_WIDTH is 128 and SCREEN_HEIGHT is 64 (or 32 for smaller displays) |
| Both devices not found | I2C wiring is disconnected | Check that SDA goes to GPIO 21 and SCL goes to GPIO 22. Ensure both devices share a common GND |