← Scrimptech ☰ Contents
Complete Build Guide

AHT25 Web Monitor: Live Temp + Humidity Dashboard

An ESP32 reads temperature (°F & °C) and humidity from an AHT25 I2C sensor, syncs a clock via NTP, and serves a dark-themed web dashboard plus a JSON API — all for under $5 in parts.

Platform: ESP32 (WROOM-32) · Framework: Arduino · Sensor: AHT25 (I2C) · Web: ESP32 WebServer · ~20 min

⚡ Quick Start

  1. Wire — connect AHT25: VDD→3V3, GND→GND, SDA→GPIO21, SCL→GPIO22
  2. Flash — create src/credentials.h with your WiFi creds, run pio run -t upload
  3. Open — check the serial monitor for the IP address, then visit http://<IP>/ in a browser

Full details below ⬇

1. What It Does

The aht25-web-monitor project turns an ESP32 into a networked weather station. An AHT25 temperature and humidity sensor sits on the I2C bus, and the ESP32 reads it every 2 seconds and serves the data two ways:

  • Web dashboard at / — a dark-themed, auto-refreshing page showing temperature in °F (primary) and °C, humidity as a percentage, the current time synced via NTP, and the sensor status.
  • JSON API at /json — a machine-readable endpoint returning temperature, humidity, time, and sensor status, ready to be consumed by another ESP32, a phone app, or a home automation system.

Why AHT25

The AHT25 is a calibrated I2C sensor with ±2% RH and ±0.3°C accuracy. It's cheap (~$2), tiny, and reads over the same 2-wire bus as hundreds of other sensors — no ADC, no signal conditioning.

Why a JSON API

A second ESP32 can poll /json over WiFi and use the readings to trigger fans, displays, or alerts — no cloud service needed, no API keys, no rate limits.

💡 WiFi credentials are never hardcoded. They live in a gitignored src/credentials.h file that's excluded from version control.

2. How It Works

Four components pass the data along a simple chain:

🌡️ AHT25 I2C sensor temp + humidity I2C ⚡ ESP32 reads sensor every 2 s NTP clock via WiFi serves web dashboard serves /json API WebServer on port 80 HTTP JSON 💻 Dashboard auto-refresh 5 s °F / °C + humidity 💾 JSON API machine-readable other ESP32 / apps

The data flow

The ESP32 uses the Wire library to talk to the AHT25 over I2C at address 0x38. Every 2 seconds, loop() calls aht.read(tempC, humidity) which returns Celsius and relative humidity. The web handler converts to °F and builds the HTML. The JSON handler builds a simple JSON string with all values. NTP syncs the clock in the background so the dashboard always shows the correct time.

3. Parts You Need

The parts list is tiny — just the board, the sensor, and three jumper wires:

ItemWhyNotes
ESP32 DevKit V1 (38-pin)Runs the firmware; has WiFi + I2CAny ESP32-WROOM-32 board with GPIO21 & GPIO22 broken out
AHT25 breakout boardTemperature + humidity sensor3.3V–5V I2C, address 0x38
3x female-to-female jumper wiresConnects sensor to the ESP32SDA, SCL, VCC, GND (4 wires total)
USB cable (data, not charge-only)Power + serial debugmicro-USB or USB-C depending on your board
⚠️ A "charge-only" USB cable will power the board but won't show the serial monitor. If pio device monitor shows nothing, try a different cable.

4. Hardware & Wiring

The ESP32 DevKit V1

ESP32 DevKit V1 pinout diagram

ESP32 DevKit V1 pinout (38-pin variant). The pins highlighted in cyan are the I2C pins used by this project: GPIO21 (SDA) and GPIO22 (SCL).

The AHT25 sensor

AHT25 temperature humidity sensor pinout

AHT25 sensor pinout (bottom view). Four pins: VDD (power), SDA (data), GND (ground), SCL (clock). The sensing hole on top is where air enters for the MEMS element.

Wiring

Connect the AHT25 to the ESP32 with four jumper wires:

AHT25 PinESP32 PinGPIO
VDD3V33.3V power
SDAGPIO21I2C data
GNDGNDGround
SCLGPIO22I2C clock
💡 No pull-up resistors needed — the ESP32's internal pull-ups on GPIO21/GPIO22 are sufficient for the AHT25 at 100 kHz I2C.

5. Toolchain Setup

1

Install VS Code

Download and install Visual Studio Code for your OS.

2

Install the PlatformIO extension

In VS Code, open Extensions (Ctrl+Shift+X), search for PlatformIO IDE, and install it. PlatformIO handles the entire ESP32 toolchain — compiler, libraries, flashing — so you don't need Arduino IDE or ESP-IDF separately.

3

Clone the repo

git clone https://github.com/charlesscrimpshire/aht25.git
cd aht25
4

Open in PlatformIO

In VS Code: File → Open Folder, pick the aht25 folder. PlatformIO auto-detects the platformio.ini and loads the project.

6. Project Structure

Four files, one of which is gitignored:

aht25/
├── platformio.ini          ← build config (board, framework, libraries)
├── .gitignore              ← excludes credentials.h and build artifacts
└── src/
    ├── aht25.h             ← AHT25 sensor driver wrapper class
    ├── main.cpp            ← WiFi, NTP, web server, JSON API, sensor reads
    └── credentials.h       ← YOUR WiFi creds (gitignored, never committed)

7. platformio.ini Explained

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
    adafruit/Adafruit AHTX0@^2.0.3    ; AHT20/AHT25 sensor library
SettingWhat it does
board = esp32devTargets the generic ESP32-WROOM-32 DevKit
framework = arduinoUses the Arduino framework (simple APIs)
monitor_speed = 115200Baud rate for the serial debug console
Adafruit AHTX0Library that handles the AHT25 I2C protocol

8. Credentials (Secure)

WiFi credentials are stored in a separate file that's gitignored and never committed to GitHub:

1

Create src/credentials.h

Create the file src/credentials.h with your WiFi details:

#pragma once
#define WIFI_SSID "YourNetworkName"
#define WIFI_PASS "YourPassword"
⚠️ This file is excluded by .gitignore. Never hardcode credentials in main.cpp — always keep them in this separate, untracked file.

9. Code Walkthrough

9.1 The AHT25 wrapper (aht25.h)

class AHT25 {
public:
    bool begin(uint8_t addr = 0x38, int sda = 21, int scl = 22) {
        Wire.begin(sda, scl);
        Wire.setClock(100000);
        return _sensor.begin(&Wire);
    }
    bool read(float &tempC, float &humidity) {
        sensors_event_t humidityEvent, tempEvent;
        _sensor.getEvent(&humidityEvent, &tempEvent);
        if (isnan(tempEvent.temperature) || isnan(humidityEvent.relative_humidity))
            return false;
        tempC = tempEvent.temperature;
        humidity = humidityEvent.relative_humidity;
        return true;
    }
};

A tiny wrapper around the Adafruit AHTX0 library. begin() initializes I2C on the specified pins and starts the sensor. read() returns false if the sensor fails to respond.

9.2 WiFi + NTP (in setup)

WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(500);

configTime(GMT_OFFSET, DST_OFFSET, NTP_SERVER);  // pool.ntp.org

The board connects to WiFi, then configures NTP in the background. The clock offset is set for CST (UTC-6) with +1h daylight saving. NTP keeps the time accurate without any RTC hardware.

9.3 The web dashboard

void handleRoot() {
    String tempF = String(temperature * 9.0f / 5.0f + 32.0f, 1);
    // ... builds HTML with tempF, humidity, getTimeStr() ...
    server.send(200, "text/html; charset=utf-8", html);
}

The dashboard HTML is built as a raw string literal, injected with live values. It uses a <meta http-equiv="refresh" content="5"> tag to auto-reload every 5 seconds. The charset=utf-8 Content-Type ensures the ° symbol renders correctly.

9.4 The JSON API

String buildJSON() {
    String json = "{";
    json += "\"time\":\"" + String(timeBuf) + "\",";
    json += "\"temperature_c\":" + String(tempBuf) + ",";
    json += "\"temperature_f\":" + String(temperature * 9.0f / 5.0f + 32.0f, 1) + ",";
    json += "\"humidity\":" + String(humBuf) + ",";
    json += "\"sensor_ok\":" + String(sensorOK ? "true" : "false");
    json += "}";
    return json;
}

No JSON library needed — just string concatenation. The API returns all values in a single object, making it easy to parse on another ESP32 with ArduinoJson or even raw string parsing.

9.5 The loop

void loop() {
    server.handleClient();   // handle web requests
    readSensor();            // read AHT25 every 2 seconds
}

Non-blocking by design. readSensor() only runs every READ_INTERVAL (2000 ms), so the web server stays responsive between reads.

10. Build & Flash

1

Plug in the board

Connect your ESP32 with a data USB cable. On Linux, check the port:

ls /dev/ttyUSB*  # typical CH340 serial port
2

Build and flash

pio run -t upload

PlatformIO auto-detects the serial port. If it picks the wrong one, set it in platformio.ini:

upload_port = /dev/ttyUSB0
3

Watch the serial monitor

pio device monitor

You should see:

=== AHT25 Web Monitor ===
AHT25: OK
Connecting to YourNetwork...
WiFi: 192.168.0.102
NTP: 2026-08-19 23:09:42 CDT
Web server: started
💡 On Linux you may need to be in the dialout group: sudo usermod -a -G dialout $USER, then log out and back in.

11. Using It

1

Open the dashboard

Visit http://<ESP32-IP>/ in any browser. The page auto-refreshes every 5 seconds and shows:

  • Temperature in °F (large) and °C (small)
  • Humidity as a percentage
  • Time synced via NTP
  • Sensor status — OK or ERROR
2

Query the JSON API

curl http://192.168.0.102/json

Returns:

{
  "time": "2026-08-19T23:09:42",
  "temperature_c": 27.3,
  "temperature_f": 81.1,
  "humidity": 54.2,
  "sensor_ok": true
}
3

Use from another ESP32

Another ESP32 can fetch the JSON over WiFi and parse it with ArduinoJson:

HTTPClient http;
http.begin("http://192.168.0.102/json");
int code = http.GET();
String payload = http.getString();
// parse JSON with ArduinoJson...

12. Troubleshooting

SymptomLikely causeFix
AHT25: FAILEDWiring wrong, or sensor not on I2CCheck SDA→GPIO21, SCL→GPIO22; run an I2C scanner sketch
WiFi: FAILEDWrong SSID/password, or out of rangeVerify credentials.h; move closer to the router
NTP: sync timeoutDNS or internet issueCheck WiFi connection; try a different NTP server
Dashboard shows ° symbol as garbageMissing charset in Content-TypeEnsure text/html; charset=utf-8 in server.send()
JSON returns 0.0 for everythingSensor reads NaN (not connected)Check wiring, verify I2C address is 0x38
Serial monitor shows nothingWrong baud rate or charge-only cableSet monitor to 115200; try a data cable

13. Next Steps & Ideas

  • Data logging — write readings to SPIFFS or an SD card with timestamps for historical charts.
  • MQTT — push readings to a Home Assistant / Mosquitto broker for home automation.
  • OTA updates — add ArduinoOTA so you can flash new firmware over WiFi without plugging in USB.
  • Multiple sensors — chain AHT25, BME280, or BMP280 on the same I2C bus and show all readings.
  • Alerts — send an email or push notification when temperature crosses a threshold.
  • Deep sleep — wake every 5 minutes, read, report, and sleep to run on battery for months.
View source on GitHub →