ESP32 BLE Weather Station

Build a wireless weather station and view live data in Chrome.

Introduction

30 Second Summary

You probably check the weather on your phone every morning. That data comes from sensors just like the one you are about to build. The difference is yours will broadcast live from your desk to your browser over Bluetooth.

In this project, you will wire a BME280 sensor to an ESP32 microcontroller, program it to broadcast temperature, humidity, and pressure over Bluetooth Low Energy, and build a web dashboard in Chrome that connects directly to the hardware and displays live readings. This is Stage 1 of the IoT Weather Station Series, which later takes your data online with S3 hosting, serverless APIs, automated testing, live alerts, and enterprise security.

What You'll Build

You will hold up your laptop, click one button on a web page, and watch your browser pair directly with a piece of hardware on your desk to stream live environmental data with no server, no WiFi, and no app install required.

By the end of this project, you'll have:

  • A physical sensor circuit you wired yourself that reads real temperature, humidity, and barometric pressure from the air around you.
  • A custom ESP32 firmware that advertises a BLE Environmental Sensing Service so any Bluetooth device nearby can subscribe to live weather data.
  • A styled web dashboard that pairs with your hardware straight from Chrome and shows live-updating readings you can demo to anyone by opening a single HTML file.
  • Secret Mission: Add a live-updating temperature chart that plots readings over time using HTML Canvas.

Are there any prerequisites?

No prior hardware or embedded programming experience is needed. This is the starting point of the IoT Weather Station Series, so no earlier projects are required.

You will need an ESP32 dev board, a BME280 sensor module, a breadboard, and jumper wires. These are available together in sensor kits on Amazon for around $10-15.

Before We Start

You are about to combine three different disciplines in a single project: hardware wiring, embedded C++ programming, and frontend web development. Taking a moment now to articulate what you are building and why helps you stay oriented when switching between these very different modes of work.

Install Arduino IDE and Configure ESP32 Support

Your ESP32 is a powerful microcontroller, but it cannot run any code until you have a development environment that can compile firmware and upload it to the chip.

Arduino IDE is the most beginner-friendly tool for programming microcontrollers. In this step, you will install it, add the ESP32 board package (which gives Arduino IDE the compiler for your specific chip), and install the sensor library you need for the BME280.

In this step, get ready to:
  • Download and install Arduino IDE on your Windows PC.
  • Add ESP32 board support via the Boards Manager.
  • Install the BME280 sensor library and connect your ESP32 via USB.
Download and install Arduino IDE

Arduino IDE is the application where you will write, compile, and upload code to your ESP32. You need the Windows 64-bit installer.

  • Open your browser and go to the Arduino IDE download page:
  • https://www.arduino.cc/en/software
  • Click the Windows Win 10 and newer, 64 bits download option.
  • Run the downloaded .exe file.
  • Accept the license agreement when prompted.
  • Click through the remaining prompts using the default settings, then click Install.
  • If Windows asks to install USB drivers during the process, click Install to allow them.

Why USB drivers?

Arduino IDE communicates with your ESP32 over a USB-to-serial connection. The drivers let your PC detect the board as a serial device on a COM port.

  • Once installation finishes, open Arduino IDE from your Start menu or Desktop shortcut.

You should see the Arduino IDE editor window with an empty sketch containing setup() and loop() functions.

Arduino IDE won't open?

If Windows SmartScreen blocks the app, click More info then Run anyway. The installer is safe and signed by Arduino.

help me get Arduino IDE running on Windows

Add ESP32 board support

Arduino IDE does not know about ESP32 chips out of the box. You need to tell it where to find the ESP32 board definitions by adding a URL, then install the board package from that source.

  • In Arduino IDE, click File in the top menu bar.
  • Click Preferences.
  • Find the Additional boards manager URLs field near the bottom of the Preferences window.
  • Paste the following URL into that field:
https://espressif.github.io/arduino-esp32/package_esp32_index.json
  • Click OK to close Preferences.

What is this URL?

This is Espressif's official package index. It tells Arduino IDE where to download the ESP32 compiler, core libraries, and board definitions. Without it, the IDE has no idea how to compile code for your chip.

  • Click Tools in the top menu bar.
  • Hover over Board, then click Boards Manager.
  • In the search box at the top of Boards Manager, type esp32.
  • Find esp32 by Espressif Systems in the results.
  • Click Install. This download is around 300 MB and may take a few minutes.

Once the installation finishes, you should see the word INSTALLED next to the esp32 package in Boards Manager.

Download seems stuck or fails?

The ESP32 package is large. If the download stalls, close Boards Manager, go back to File > Preferences, verify the URL is pasted correctly with no extra spaces, click OK, and try again.

help me install the ESP32 board package in Arduino IDE

Install the BME280 library and connect your ESP32

The BME280 sensor communicates over a protocol called I2C. Adafruit provides a library that handles all the low-level I2C communication so you can read temperature, humidity, and pressure with simple function calls.

  • In Arduino IDE, click Sketch in the top menu bar.
  • Hover over Include Library, then click Manage Libraries.
  • In the search box, type Adafruit BME280.
  • Find Adafruit BME280 Library in the results and click Install.
  • When prompted to install dependencies, click Install All. This installs the Adafruit Unified Sensor library that BME280 depends on.

What is a library dependency?

The Adafruit BME280 library uses shared code from the Adafruit Unified Sensor library. Installing the dependency ensures all the pieces work together. Think of it like installing an app that also needs a specific system update to function.

Now connect your ESP32 hardware so the IDE can communicate with it.

  • Plug your ESP32 dev board into your PC using a USB cable.
  • In Arduino IDE, click Tools in the top menu bar.
  • Hover over Board, then hover over esp32, then click ESP32 Dev Module.
  • Click Tools again, then click Port.
  • Select the COM port that appeared after you plugged in your ESP32 (it will look something like COM3 or COM4).

No COM port showing up?

Try a different USB cable. Some cables are charge-only and do not carry data. If you still see no port, you may need to install the CP2102 or CH340 USB-to-serial driver for your specific ESP32 board. Check the back of your ESP32 for the chip label near the USB connector.

help me find my ESP32 COM port in Arduino IDE on Windows

Before you check, do you expect to see ESP32 Dev Module listed under Tools > Board > esp32?

  • Confirm your setup by clicking Tools > Board > esp32.

You should see ESP32 Dev Module listed and selected. You should also see a COM port number displayed at the bottom of the Arduino IDE window. Both confirm your environment is fully configured.

Your development environment is ready to go. Next up, you will wire the BME280 sensor to your ESP32 and see real weather data appear in the Serial Monitor.

Wire the BME280 Sensor and Read Weather Data

Your development environment is ready. Now it is time to connect the physical sensor and prove it can talk to your ESP32.

Before adding Bluetooth complexity, you need to confirm the BME280 is wired correctly and returning valid readings. Seeing real temperature and humidity numbers in Serial Monitor is your first visible proof that hardware and software are working together.

In this step, get ready to:
  • Wire the BME280 sensor to your ESP32 on a breadboard.
  • Upload a test sketch that reads temperature, humidity, and pressure.
  • See live sensor data printing in Serial Monitor.
Wire the BME280 sensor to the ESP32

The BME280 communicates with the ESP32 using a protocol called I2C. It only needs four wires: power, ground, and two data lines.

  • Place your BME280 module onto the breadboard so its pins slot into separate rows.
  • Connect the BME280 VCC pin to the ESP32 3.3V pin using a jumper wire.
  • Connect the BME280 GND pin to the ESP32 GND pin.
  • Connect the BME280 SCL pin to ESP32 GPIO 22.
  • Connect the BME280 SDA pin to ESP32 GPIO 21.
  • Double-check each connection is firm and that no bare wires are touching each other on the breadboard.

What is I2C?

I2C (pronounced "eye-squared-see") is a communication protocol that lets multiple sensors share just two data wires: SCL (clock) and SDA (data). GPIO 22 and GPIO 21 are the default I2C pins on the ESP32.

Each sensor on the bus has a unique address. Your BME280 uses address 0x76 by default.

Create and upload the sensor test sketch
  • In Arduino IDE, click File, then New Sketch.
  • Select all the default code in the editor and delete it.
  • Enter the following sensor test code that reads your BME280 and prints the results:
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Adafruit_BME280 bme;

void setup() {
  Serial.begin(115200);
  if (!bme.begin(0x76)) {
    Serial.println("Could not find BME280 sensor! Check wiring.");
    while (1);
  }
  Serial.println("BME280 sensor found.");
}

void loop() {
  float temp = bme.readTemperature();
  float hum = bme.readHumidity();
  float presRaw = bme.readPressure();
  float presHpa = presRaw / 100.0F;
  Serial.printf("Temp: %.2f C | Humidity: %.2f%% | Pressure: %.2f hPa\n", temp, hum, presHpa);
  delay(2000);
}

What does this code do?

  • bme.begin(0x76) connects to the BME280 sensor at its I2C address. If it cannot find the sensor, the sketch stops and prints an error.
  • readTemperature(), readHumidity(), and readPressure() grab the latest readings from the sensor hardware.
  • Serial.printf() formats and sends the values to your PC over the USB cable so you can see them in Serial Monitor.
  • delay(2000) pauses for 2 seconds between each reading so the output is readable.
  • Click the Upload button (the right-pointing arrow icon in the top-left toolbar) to compile and flash the sketch to your ESP32.
  • Wait for the console at the bottom to say "Done uploading." This takes around 20-30 seconds.

Before you check the output, predict: what do you think you will see if the wiring is correct? What if it is wrong?

  • Open Serial Monitor by clicking Tools, then Serial Monitor.
  • Set the baud rate dropdown (bottom-right of Serial Monitor) to 115200.
  • Press the RST button on your ESP32 board to restart it.

You should see "BME280 sensor found." followed by temperature, humidity, and pressure values printing every 2 seconds. Try breathing gently on the sensor to watch the humidity value spike.

Seeing "Could not find BME280 sensor"?

  • Some BME280 modules use I2C address 0x77 instead of 0x76. In the code, change bme.begin(0x76) to bme.begin(0x77) and re-upload.
  • Double-check that SCL goes to GPIO 22 and SDA goes to GPIO 21. Swapping these two wires is the most common wiring mistake.
  • Make sure the VCC wire goes to the ESP32 3.3V pin, not the 5V pin. The BME280 runs on 3.3V.

Still stuck? Help me troubleshoot my BME280 sensor connection

✔️ Awesome, I've got everything!

Great. Double-check you see live readings updating every 2 seconds before moving on.

ⓧ I'd like to double check the full code

Here is the complete weather_station.ino sketch. Make sure your code matches exactly:

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Adafruit_BME280 bme;

void setup() {
  Serial.begin(115200);
  if (!bme.begin(0x76)) {
    Serial.println("Could not find BME280 sensor! Check wiring.");
    while (1);
  }
  Serial.println("BME280 sensor found.");
}

void loop() {
  float temp = bme.readTemperature();
  float hum = bme.readHumidity();
  float presRaw = bme.readPressure();
  float presHpa = presRaw / 100.0F;
  Serial.printf("Temp: %.2f C | Humidity: %.2f%% | Pressure: %.2f hPa\n", temp, hum, presHpa);
  delay(2000);
}

Your sensor is reading real environmental data and sending it to your PC over USB. Next up, you will cut that USB tether by adding Bluetooth Low Energy so the ESP32 can broadcast these readings wirelessly to any nearby device.

Add BLE to Broadcast Weather Data

Your weather station reads real sensor data, but it is trapped behind a USB cable. You can only view temperature and humidity through Serial Monitor on the computer the ESP32 is physically plugged into.

Adding Bluetooth Low Energy (BLE) frees those readings from the cable. Once BLE is active, the ESP32 broadcasts sensor data wirelessly so any BLE-capable device nearby can connect and receive live updates.

In this step, get ready to:
  • Replace the sensor test sketch with full BLE firmware that creates an Environmental Sensing Service.
  • Upload the firmware and confirm the ESP32 is advertising over Bluetooth.
  • Observe that the data is now broadcast wirelessly, ready for a client to connect.
Replace your sketch with the BLE weather station code

You are going to replace the entire contents of your current sketch with new firmware that adds a BLE radio layer on top of the sensor readings you already have. The new code is longer, so you will enter it in sections.

  • In Arduino IDE, select all the code in your current sketch with Ctrl+A and delete it.
  • Enter the following code at the top of the now-empty sketch:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

// BME280 sensor object (I2C)
Adafruit_BME280 bme;

// BLE server name (visible when scanning)
#define bleServerName "ESP32_Weather"

// Environmental Sensing Service UUID (BLE SIG standard)
#define SERVICE_UUID BLEUUID((uint16_t)0x181A)

What does this code do?

The first three includes bring in the ESP32's built-in BLE stack. You do not need to install these separately.

The bleServerName define sets the device name that appears when other devices scan for Bluetooth. SERVICE_UUID uses the official BLE SIG Environmental Sensing Service identifier (0x181A), which tells clients this device provides weather data.

  • Directly below the code you just entered, add the BLE characteristic declarations:
// Temperature Characteristic (BLE SIG: 0x2A6E)
BLECharacteristic temperatureCharacteristic(
  BLEUUID((uint16_t)0x2A6E),
  BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
);
BLEDescriptor temperatureDescriptor(BLEUUID((uint16_t)0x2902));

// Humidity Characteristic (BLE SIG: 0x2A6F)
BLECharacteristic humidityCharacteristic(
  BLEUUID((uint16_t)0x2A6F),
  BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
);
BLEDescriptor humidityDescriptor(BLEUUID((uint16_t)0x2902));

// Pressure Characteristic (BLE SIG: 0x2A6D)
BLECharacteristic pressureCharacteristic(
  BLEUUID((uint16_t)0x2A6D),
  BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
);
BLEDescriptor pressureDescriptor(BLEUUID((uint16_t)0x2902));

bool deviceConnected = false;

What are these characteristics?

In BLE, a characteristic is a single data point a device exposes. Each one here uses an official BLE SIG UUID: 0x2A6E for temperature, 0x2A6F for humidity, and 0x2A6D for pressure.

PROPERTY_READ lets a client request the current value on demand. PROPERTY_NOTIFY lets the ESP32 push updates automatically whenever new readings are available. The 0x2902 descriptor enables clients to subscribe to those notifications.

  • Below the declarations, add the connection callback class:
// Callback for BLE connection events
class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
    Serial.println("Client connected!");
  }
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    Serial.println("Client disconnected!");
    pServer->getAdvertising()->start();
    Serial.println("Advertising restarted...");
  }
};

What does the callback class do?

BLE triggers events when a client connects or disconnects. This class handles both: it sets deviceConnected to true when someone pairs, and restarts advertising when they leave so the ESP32 is discoverable again.

  • Next, add the setup() function that initializes both the sensor and the BLE stack:
void setup() {
  Serial.begin(115200);
  // Initialize BME280 sensor
  if (!bme.begin(0x76)) {
    Serial.println("Could not find BME280 sensor! Check wiring.");
    while (1);
  }
  Serial.println("BME280 sensor found.");
  // Initialize BLE
  BLEDevice::init(bleServerName);
  // Create BLE Server
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());
  // Create Environmental Sensing Service
  BLEService *weatherService = pServer->createService(SERVICE_UUID);
  // Add characteristics with notification descriptors
  weatherService->addCharacteristic(&temperatureCharacteristic);
  temperatureCharacteristic.addDescriptor(&temperatureDescriptor);
  weatherService->addCharacteristic(&humidityCharacteristic);
  humidityCharacteristic.addDescriptor(&humidityDescriptor);
  weatherService->addCharacteristic(&pressureCharacteristic);
  pressureCharacteristic.addDescriptor(&pressureDescriptor);
  // Start service and begin advertising
  weatherService->start();
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);
  BLEDevice::startAdvertising();
  Serial.println("BLE Weather Station is advertising...");
}

What does this setup do?

The first section is identical to your old sketch: it initializes the BME280 sensor over I2C. The new BLE section creates a server, attaches the three characteristics to the Environmental Sensing Service, and starts advertising.

Once startAdvertising() runs, nearby BLE devices can discover the ESP32 by its name ESP32_Weather.

  • Finally, add the loop() function that reads the sensor and broadcasts over BLE:
void loop() {
  if (deviceConnected) {
    // Read sensor values
    float temp = bme.readTemperature();
    float hum = bme.readHumidity();
    float presRaw = bme.readPressure();
    float presHpa = presRaw / 100.0F;
    // Format for BLE ESS: temperature as int16 in 0.01 degC units
    int16_t tempBLE = (int16_t)(temp * 100);
    temperatureCharacteristic.setValue((uint8_t*)&tempBLE, 2);
    temperatureCharacteristic.notify();
    // Format for BLE ESS: humidity as uint16 in 0.01% units
    uint16_t humBLE = (uint16_t)(hum * 100);
    humidityCharacteristic.setValue((uint8_t*)&humBLE, 2);
    humidityCharacteristic.notify();
    // Format for BLE ESS: pressure as uint32 in 0.1 Pa units
    uint32_t presBLE = (uint32_t)(presRaw * 10);
    pressureCharacteristic.setValue((uint8_t*)&presBLE, 4);
    pressureCharacteristic.notify();
    Serial.printf("Temp: %.2f C | Humidity: %.2f%% | Pressure: %.2f hPa\n",
                  temp, hum, presHpa);
    delay(2000);
  }
  delay(100);
}

What does this loop do?

The loop only sends data when a client is connected (deviceConnected is true). It reads the sensor, converts each value into the BLE-standard integer format, then calls notify() to push the update to the connected client.

The BLE SIG specification requires temperature as an int16 in 0.01 degree units, humidity as uint16 in 0.01% units, and pressure as uint32 in 0.1 Pa units. Multiplying then casting handles this encoding.

  • Press Ctrl+S to save the sketch.

✔️ Awesome, I've got everything!

Great. Confirm you have saved the file and your sketch contains all five sections: includes/defines, characteristic declarations, callback class, setup(), and loop().

ⓧ I'd like to double check the full code

Scroll through your sketch and confirm these sections appear in order:

  • Six #include lines (BLEDevice, BLEServer, BLE2902, Wire, Adafruit_Sensor, Adafruit_BME280).
  • bme object, bleServerName define, SERVICE_UUID define.
  • Three characteristic objects (temperatureCharacteristic, humidityCharacteristic, pressureCharacteristic) each with a descriptor.
  • bool deviceConnected = false; line.
  • MyServerCallbacks class with onConnect and onDisconnect methods.
  • setup() ending with Serial.println("BLE Weather Station is advertising...");.
  • loop() ending with delay(100); outside the if block.
Upload and verify BLE advertising

With the full BLE firmware saved, you can now compile and upload it to the ESP32. The BLE libraries add significant size to the compiled binary compared to your old sensor-only sketch.

  • Click the Upload button (right arrow icon) in the Arduino IDE toolbar.

Seeing a "sketch too large" error?

BLE libraries use more flash memory than the default partition allows. Fix this by changing the partition scheme.

  • Go to Tools > Partition Scheme.
  • Select Huge APP (3MB No OTA/1MB SPIFFS).
  • Click Upload again.

If you do not see this option, confirm your board is set to ESP32 Dev Module under Tools > Board.

Help me fix a sketch too large error on my ESP32 BLE project.

Before you check Serial Monitor, predict: what message do you expect the ESP32 to print once the upload finishes and the board restarts?

  • Open Serial Monitor (Tools > Serial Monitor) and confirm the baud rate is set to 115200.
  • Press the RST button on your ESP32 to restart it.

You should see two messages: BME280 sensor found. followed by BLE Weather Station is advertising.... The ESP32 is now broadcasting over Bluetooth.

Not seeing the advertising message?

If you see Could not find BME280 sensor! the wiring issue is unrelated to BLE. Double-check your jumper wires match: VCC to 3.3V, GND to GND, SCL to GPIO 22, SDA to GPIO 21.

If you see garbled text, confirm Serial Monitor is set to 115200 baud.

If the upload itself fails with a connection timeout, hold the BOOT button on the ESP32 while clicking Upload, and release it once you see "Connecting..." in the output.

Help me troubleshoot my ESP32 BLE upload.

💡 Why is BLE Environmental Sensing Service better than custom UUIDs?

The UUIDs you used (0x181A, 0x2A6E, 0x2A6F, 0x2A6D) are assigned by the Bluetooth SIG as industry standards. Any BLE client that understands these UUIDs can automatically interpret your data format without custom documentation.

Custom UUIDs work, but they require every client to know your proprietary encoding. Using the standard is what companies do in production IoT devices.

Your ESP32 is now a wireless weather station, broadcasting live sensor data over Bluetooth. The Serial Monitor still prints readings, but the whole point is to view them from another device without a cable. In the next step, you will build a web page in Chrome that connects directly to the ESP32 and displays live-updating weather data on screen.

Build the Web Bluetooth Dashboard

Your ESP32 is broadcasting live weather data over Bluetooth, but right now the only way to see it is through the Serial Monitor with a USB cable attached.

The Web Bluetooth API lets Chrome connect directly to your ESP32's BLE service, subscribe to notifications, and display live sensor updates on a styled web page. This is the frontend that completes your fullstack weather station.

In this step, get ready to:
  • Build a styled HTML dashboard with cards for temperature, humidity, and pressure.
  • Write JavaScript that pairs with the ESP32 using Web Bluetooth and subscribes to BLE notifications.
  • See live sensor readings updating in your browser every 2 seconds.
Create the dashboard file and add page styles

Your dashboard lives in a single HTML file with embedded CSS and JavaScript. Chrome treats local file:// URLs as a secure context, so Web Bluetooth works without needing a web server.

  • Create a new file called index.html on your Desktop.
  • Open it in a text editor (Notepad or any editor you prefer).
  • Add the opening HTML structure and base page styles by pasting this code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32 Weather Station</title>
    <style>
        /* Reset default browser spacing */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        /* Full-page gradient background, centered content */
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 20px;
        }

What does this code do?

This sets up the HTML document and starts the stylesheet. The body rule creates a purple-to-blue gradient that fills the entire screen, with flexbox centering everything vertically and horizontally.

  • Continue building the stylesheet by adding the heading, status text, button, and card styles directly below the previous code:
        /* Page title */
        h1 {
            color: white;
            margin-bottom: 10px;
            font-size: 2rem;
        }
        /* Connection status message */
        #status {
            color: rgba(255, 255, 255, 0.9);
            margin-bottom: 20px;
            font-size: 1.1rem;
        }
        /* Connect button with hover and disabled states */
        #connect-btn {
            background: white;
            color: #667eea;
            border: none;
            padding: 15px 30px;
            font-size: 1.1rem;
            border-radius: 8px;
            cursor: pointer;
            font-weight: bold;
            margin-bottom: 30px;
            transition: transform 0.2s;
        }
        #connect-btn:hover {
            transform: scale(1.05);
        }
        #connect-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
            transform: none;
        }

What does this code do?

These rules style the UI elements the learner will interact with. The button grows slightly on hover to give tactile feedback. When disabled (after connecting), it greys out and removes the pointer cursor.

  • Save index.html and open it in Chrome by double-clicking the file or dragging it into a Chrome tab.

You should see a purple-to-blue gradient filling the entire browser window. No cards or text appear yet because you have not added the HTML body content.

Seeing a blank white page?

Make sure you saved the file with the .html extension (not .txt). If you used Notepad, choose All Files in the Save As dialog and type the filename as index.html including the extension.

help me figure out why my index.html page is showing a blank white background instead of the purple gradient

  • Return to your text editor and add the card grid styles below the previous CSS, then close the style and head tags:
        /* Responsive grid for weather cards */
        .cards {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
            width: 100%;
            max-width: 700px;
        }
        /* Individual card styling */
        .card {
            background: white;
            border-radius: 12px;
            padding: 25px;
            text-align: center;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
        }
        .card-label {
            font-size: 0.9rem;
            color: #888;
            text-transform: uppercase;
            letter-spacing: 1px;
        }
        .card-value {
            font-size: 2.5rem;
            font-weight: bold;
            color: #333;
            margin: 10px 0;
        }
        .card-unit {
            font-size: 1rem;
            color: #666;
        }
    </style>
</head>

What does this code do?

The .cards grid uses auto-fit with a minimum of 200px per column, so cards automatically stack on narrow screens. Each .card gets a white background with rounded corners and a shadow to float above the gradient.

Add the HTML body structure
  • Below the closing </head> tag you just added, paste the HTML body content:
<body>
    <h1>ESP32 Weather Station</h1>
    <p id="status">Click Connect to pair with your ESP32</p>
    <button id="connect-btn" onclick="connectToDevice()">Connect via Bluetooth</button>

    <!-- Three cards showing live sensor readings -->
    <div class="cards">
        <div class="card">
            <div class="card-label">Temperature</div>
            <div class="card-value" id="temperature">--</div>
            <div class="card-unit">C</div>
        </div>
        <div class="card">
            <div class="card-label">Humidity</div>
            <div class="card-value" id="humidity">--</div>
            <div class="card-unit">%</div>
        </div>
        <div class="card">
            <div class="card-label">Pressure</div>
            <div class="card-value" id="pressure">--</div>
            <div class="card-unit">hPa</div>
        </div>
    </div>

What does this code do?

The button calls connectToDevice() when clicked (you will write this function next). Each card has a card-value element with an id that JavaScript will target to inject live readings. The -- placeholder shows until the ESP32 sends its first data packet.

  • Save index.html and refresh Chrome.

You should now see the full dashboard layout: a white Connect via Bluetooth button, a status message, and three white cards showing -- for Temperature, Humidity, and Pressure.

Cards not showing or layout broken?

Check that the </head> tag appears on its own line directly before <body>. A missing closing tag can cause Chrome to misparse the document.

help me fix my HTML layout where cards are not appearing or look broken

Add the Web Bluetooth JavaScript

The JavaScript connects to your ESP32 using the Web Bluetooth API. It filters for the device by name, subscribes to BLE notifications on each characteristic, and decodes the raw bytes into human-readable values.

  • Below the closing </div> of the cards section, add the opening script tag with the BLE service constants and the first part of the connection function:
    <script>
        // BLE UUIDs matching the ESP32 firmware
        const ENV_SENSING_SERVICE = 0x181A;
        const TEMPERATURE_CHAR = 0x2A6E;
        const HUMIDITY_CHAR = 0x2A6F;
        const PRESSURE_CHAR = 0x2A6D;

        let device;
        let server;

        async function connectToDevice() {
            try {
                document.getElementById('status').textContent = 'Searching for ESP32 Weather Station...';

                // Ask browser to scan for our ESP32 by name
                device = await navigator.bluetooth.requestDevice({
                    filters: [{ name: 'ESP32_Weather' }],
                    optionalServices: [ENV_SENSING_SERVICE]
                });

                document.getElementById('status').textContent = 'Connecting...';
                server = await device.gatt.connect();

                document.getElementById('status').textContent = 'Getting weather service...';
                const service = await server.getPrimaryService(ENV_SENSING_SERVICE);

What does this code do?

The UUID constants (0x181A, 0x2A6E, etc.) match exactly what your ESP32 firmware advertises. The requestDevice call tells Chrome to show a pairing popup filtered to the device named ESP32_Weather.

Listing the service UUID in optionalServices is required. Without it, Chrome blocks access to the service after pairing even though the device advertises it.

  • Continue the connection function by adding the notification subscriptions and error handling directly below:
                // Subscribe to temperature notifications
                const tempChar = await service.getCharacteristic(TEMPERATURE_CHAR);
                await tempChar.startNotifications();
                tempChar.addEventListener('characteristicvaluechanged', handleTemperature);

                // Subscribe to humidity notifications
                const humChar = await service.getCharacteristic(HUMIDITY_CHAR);
                await humChar.startNotifications();
                humChar.addEventListener('characteristicvaluechanged', handleHumidity);

                // Subscribe to pressure notifications
                const presChar = await service.getCharacteristic(PRESSURE_CHAR);
                await presChar.startNotifications();
                presChar.addEventListener('characteristicvaluechanged', handlePressure);

                document.getElementById('status').textContent = 'Connected! Receiving live data...';
                document.getElementById('connect-btn').textContent = 'Connected';
                document.getElementById('connect-btn').disabled = true;

                device.addEventListener('gattserverdisconnected', onDisconnected);

            } catch (error) {
                console.error('Connection failed:', error);
                document.getElementById('status').textContent = 'Connection failed: ' + error.message;
            }
        }

        // Decode temperature: int16 in 0.01 degC units
        function handleTemperature(event) {
            const value = event.target.value;
            const temp = value.getInt16(0, true) / 100;
            document.getElementById('temperature').textContent = temp.toFixed(1);
        }

What does this code do?

Each startNotifications() call tells the BLE characteristic to push updates automatically. The characteristicvaluechanged event fires every time the ESP32 sends new data (every 2 seconds).

The handleTemperature function reads the raw bytes as a signed 16-bit integer (little-endian) and divides by 100 to convert from the BLE format (0.01 degree units) back to degrees Celsius.

  • Finish the script by adding the remaining handler functions and closing tags directly below:
        // Decode humidity: uint16 in 0.01% units
        function handleHumidity(event) {
            const value = event.target.value;
            const hum = value.getUint16(0, true) / 100;
            document.getElementById('humidity').textContent = hum.toFixed(1);
        }

        // Decode pressure: uint32 in 0.1 Pa units, convert to hPa
        function handlePressure(event) {
            const value = event.target.value;
            const pres = value.getUint32(0, true) / 1000;
            document.getElementById('pressure').textContent = pres.toFixed(1);
        }

        // Reset UI when BLE connection drops
        function onDisconnected() {
            document.getElementById('status').textContent = 'Disconnected. Click Connect to reconnect.';
            document.getElementById('connect-btn').textContent = 'Connect via Bluetooth';
            document.getElementById('connect-btn').disabled = false;
            document.getElementById('temperature').textContent = '--';
            document.getElementById('humidity').textContent = '--';
            document.getElementById('pressure').textContent = '--';
        }
    </script>
</body>
</html>

Why divide by different numbers?

The BLE Environmental Sensing Service standard defines specific data formats. Temperature is int16 in 0.01 degree units (divide by 100). Humidity is uint16 in 0.01% units (divide by 100). Pressure is uint32 in 0.1 Pa units, and dividing by 1000 converts to hPa.

The onDisconnected handler resets all card values to -- and re-enables the button so the learner can reconnect without refreshing the page.

  • Save index.html.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file and move on to connecting.

ⓧ I'd like to double check the full code

Your index.html should contain all six code sections you added, in this order:

  • Opening HTML + reset/body CSS (the <!DOCTYPE html> through the closing } of the body rule).
  • Heading, status, and button CSS (h1 through #connect-btn:disabled).
  • Card grid CSS + </style></head> closing tags.
  • HTML body (<body> through the cards </div>).
  • Script opening with constants + first half of connectToDevice() + handleTemperature.
  • Remaining handlers (handleHumidity, handlePressure, onDisconnected) + </script></body></html>.

Scroll up and confirm each section is present and in order. The file should start with <!DOCTYPE html> and end with </html>.

Connect to your ESP32 from Chrome

Your dashboard is ready. Before you click Connect, think about what you expect to happen. The JavaScript calls navigator.bluetooth.requestDevice() which triggers a browser-native pairing popup.

  • Make sure your ESP32 is still plugged in via USB and running the BLE weather station firmware (Serial Monitor should show "BLE Weather Station is advertising...").
  • Refresh index.html in Chrome.

What do you think will happen when you click the Connect button? Will Chrome immediately show sensor data, or will there be an intermediate step first?

  • Click the Connect via Bluetooth button.
  • A browser popup appears listing nearby BLE devices. Select ESP32_Weather from the list.
  • Click Pair.

The status text changes to "Connected! Receiving live data..." and your three cards begin updating with real sensor readings every 2 seconds. Check your Arduino IDE Serial Monitor as well. You should see "Client connected!" appear.

  • Try breathing gently on the BME280 sensor.

You should see the humidity value spike in real time on your dashboard. This proves the entire chain is working: sensor reads the air, firmware encodes BLE packets, and your browser decodes and displays them wirelessly.

ESP32_Weather not showing in the popup?

Make sure the ESP32 is powered on and Serial Monitor shows "BLE Weather Station is advertising...". If it shows "Client connected!" from a previous session, press the RST button on the ESP32 to restart advertising.

Check that Bluetooth is enabled on your Windows PC (Settings > Bluetooth & devices > toggle On).

help me debug why my ESP32_Weather device is not appearing in Chrome's Bluetooth pairing popup

🙋‍♀️ Connection fails or drops immediately?

If you see a "Connection failed" message, make sure no other device (like a phone BLE scanner app) is already connected to the ESP32. Only one client can connect at a time. Disconnect any other BLE connections first, then try again.

help me fix my Web Bluetooth connection that keeps failing or dropping

Your browser is now talking directly to your hardware. No WiFi, no server, no cloud. In the next projects in the IoT Weather Station Series, you will host this dashboard on Amazon S3, store readings in DynamoDB, and build serverless APIs with Lambda. For now, try the Secret Mission to add a live temperature chart.

Secret mission

Add a Live Temperature Chart

Your dashboard shows the current temperature, but what about the trend? A single number updating in place tells you it's 23.5 degrees right now. A chart tells you the room has been warming steadily for the past minute. In this secret mission, you'll add a canvas-based line chart that plots temperature readings over time, making your weather station dramatically more impressive for demos.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with no cloud services or subscriptions. Your ESP32 is powered via USB and your web page is a local file. There are no ongoing costs.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • ESP32 with custom BLE weather station firmware (powered via USB, no ongoing cost).
  • BME280 sensor wired to ESP32 on a breadboard.
  • index.html file on your Desktop.
  • Arduino IDE with ESP32 board support and Adafruit BME280 library.

Keep everything running

No action needed. Choose this if you want to keep using your weather station as a permanent desk setup.

  • Leave the ESP32 plugged into your PC via USB. It continues broadcasting BLE data whenever powered.
  • For a portable setup, connect a USB power bank to the ESP32 and place it anywhere in your home.
  • Open index.html in Chrome any time you want to view live readings.
  • Arduino IDE stays installed in case you want to modify the firmware later.
  • Keep your setup intact for the next projects in the IoT Weather Station Series, where you will host this dashboard on Amazon S3, store telemetry in DynamoDB, and build serverless APIs with Lambda and API Gateway.

Pause - I'll come back to this later

Shut down the hardware to free up your USB port and desk space. Your firmware stays in the ESP32's flash memory and restarts automatically when you power it on again.

  • Unplug the ESP32 from USB. Your sketch remains in flash memory and resumes the moment you reconnect power.
  • Leave the BME280 wiring connected on the breadboard so you do not need to rewire later.
  • Keep index.html on your Desktop. It has no background processes and takes no resources while closed.
  • Arduino IDE can stay installed. It does not run in the background.

Delete - I don't want to use this again

Remove all project files and software from your computer. Your ESP32 hardware is yours to keep or repurpose for another project.

  • Delete the index.html file from your Desktop. Right-click it and select Delete.
  • Uninstall Arduino IDE. Open Settings, select Apps, then Apps & features. Find Arduino IDE in the list and click Uninstall.
  • Disconnect the BME280 from the breadboard. Remove the jumper wires and store all components (ESP32, BME280, breadboard, wires) together for future projects.
  • Unplug the ESP32 from USB. The firmware remains in flash memory, but the board is safe to store or reuse with a different sketch.

Nice Work!

Nice Work!

You just built a fullstack IoT weather station from silicon to screen. Your ESP32 reads real environmental data from a BME280 sensor and broadcasts it wirelessly over Bluetooth Low Energy, and your custom web dashboard connects directly to the hardware from Chrome to display live readings.

You learned how to:

  • Wire a BME280 sensor to an ESP32 over I2C and program embedded firmware that reads temperature, humidity, and pressure from real hardware.
  • Create a BLE Environmental Sensing Service using industry-standard GATT UUIDs so any compatible device can connect and receive live sensor data wirelessly.
  • Build a styled web dashboard using the Web Bluetooth API that pairs directly with hardware from Chrome with zero backend infrastructure.
  • Secret Mission: Added a live-updating temperature chart using HTML Canvas that plots readings over time as they arrive from the ESP32.

Ready to quiz yourself?