Skip to content

Repository files navigation

ESP32 Environmental Wifi Webserver

A multi-sensor environmental monitor built on the ESP32. It reads up to three I²C sensors once a minute, logs every reading to a microSD card as CSV, and serves a live, mobile-friendly web dashboard with zoomable charts — no cloud, no app, just a web page on your local network.

The instrument and its live dashboard

The sensor suite (temperature, humidity, barometric pressure, VOC/gas, O₂, and CO₂) is chosen so the same hardware works as a general room monitor today and as the foundation for gas-analysis experiments later (see Where this is going).


Features

  • Up to three I²C sensors, all optional and independent. BME680 (temperature, humidity, pressure, gas/VOC), Sensirion SCD41 (CO₂, plus its own temperature and humidity), and DFRobot Gravity SEN0322 (O₂). Run one, two, or all three — if a sensor is absent or fails to initialize, the rig keeps running, logs NaN for that sensor's columns, and the dashboard simply hides the charts that have no data. No code edits required to change the sensor set.
  • 60-second logging to microSD as a 14-column CSV (one timestamped row per reading). Append-only; at this cadence a small card holds decades of data.
  • Live web dashboard with up to six charts — temperature, humidity, pressure, gas/VOC, oxygen, and CO₂ — served directly from the ESP32.
  • Real time-axis plotting. Points are placed by their actual timestamp, so tooltips and positions always agree, and gaps (for example, across a reboot) render as clean breaks rather than misleading straight lines.
  • History survives reboots. On startup the device re-seeds its in-memory plot buffer from the tail of the CSV, so the charts come up already populated instead of blank after a power cycle or reflash.
  • Three-way units selector — °C/hPa, °F/mmHg, or °F/PSI — switchable live in the browser, with a configurable default.
  • Optional translucent "shadow" fill under each plot line, with blended overlaps on the dual-sensor temperature and humidity charts. Toggleable.
  • Pinch / wheel zoom and drag-pan on the time axis (works on desktop and mobile), with a one-click reset.
  • Download or clear the log from the dashboard. Downloads are named with a sortable timestamp (sensor_log_YYYY-MM-DD_HHMMSS.csv); clearing is password-protected.
  • Bounded, fast display regardless of file size. The dashboard reads a capped in-RAM ring buffer, so plot performance stays constant even as the on-disk log grows for months.
  • All settings in one place. Tunables live in config.h; WiFi credentials are isolated in a git-ignored secrets.h, so the project is safe to publish as-is.

Why this sensor suite

The three sensors were chosen to measure the gases and conditions that matter for biology and culture-metabolism work, while still being a useful general environmental monitor:

  • CO₂ (SCD41) is a near-universal proxy for metabolic activity — respiring organisms produce it, so its rise tracks biological activity in an enclosed volume.
  • O₂ (SEN0322) is the complementary signal: aerobic metabolism consumes it, so O₂ depletion and CO₂ accumulation together tell a coherent story.
  • VOC / gas resistance (BME680) adds a supporting channel for volatile organic compounds, useful for detecting metabolic byproducts and general air quality.
  • Temperature, humidity, and pressure provide the environmental context that every other reading must be interpreted against — and the BME680 and SCD41 each measure temperature and humidity independently, giving a built-in cross-check.

For a sealed-chamber experiment, the SCD41's automatic self-calibration should be off (it assumes periodic exposure to fresh ~400 ppm air and would otherwise corrupt an elevated, closed-system CO₂ baseline). This is a one-line setting in config.h — see Configuration.


Electronics (bill of materials)

Specific parts used in the reference build are listed below with their product descriptions, so the item is identifiable even if a purchase link goes stale. Commodity parts (jumper wires, headers, breadboard) can be substituted freely.

Part Notes
ESP32 dev board Lonely Binary ESP32 IPEX Gold Edition (external antenna + PinPulse shield with GPIO LEDs). Any ESP32 dev board works; the external antenna helps WiFi range.
BME680 sensor Temperature / humidity / pressure / gas (VOC), I²C + SPI breakout. I²C address 0x77.
SCD41 sensor Sensirion CO₂ + temperature + humidity, I²C. Address 0x62.
Gravity SEN0322 DFRobot electrochemical O₂ sensor, 0–25 % vol, I²C. Address set by an on-board dial (default 0x73). DFRobot product page is the durable reference.
microSD module SPI microSD breakout with level shifting.
microSD card 16 GB (C10/U1) is plenty; the log grows ~140 KB/day.
Breadboard, jumper wires, pin headers Standard prototyping parts.
Decoupling capacitors 100 nF + 47 µF across the 3V3 rail near the sensors, for supply stability.

All parts laid out before assembly

The physical enclosure, mounting hardware, and assembly are documented separately in HARDWARE.md.


Wiring

All three sensors share one I²C bus. The reference build uses a consistent 4-pin color convention on the sensor cables: blue = SCL, yellow = SDA, black = GND, red = 3V3.

Signal ESP32 pin Goes to
SDA (I²C data) GPIO 21 every sensor's SDA
SCL (I²C clock) GPIO 22 every sensor's SCL
3V3 3V3 every sensor's VCC
GND GND every sensor's GND
microSD CS GPIO 5 SD module CS
microSD SCK GPIO 18 SD module SCK
microSD MISO GPIO 19 SD module MISO
microSD MOSI GPIO 23 SD module MOSI

I²C addresses

Sensor Address Measures
BME680 0x77 temperature, humidity, pressure, gas/VOC
SCD41 0x62 CO₂, temperature, humidity
Gravity SEN0322 0x73 (dial; alternates 0x700x73) O₂ %

A Fritzing breadboard diagram (showing the color convention and the decoupling caps) and close-up wiring photos are in HARDWARE.md. The editable Fritzing project and its custom sensor parts are in hardware/fritzing/.

Wiring detail


Build & flash

This project builds with PlatformIO (VS Code or CLI). The same libraries also install through the Arduino IDE Library Manager if you prefer that toolchain.

  1. Clone the repository and open the folder in PlatformIO.

  2. Create your secrets.h. Copy the template and fill in your WiFi credentials:

    cp src/secrets.h.example src/secrets.h

    Then edit src/secrets.h:

    #define WIFI_SSID     "Your_Network"
    #define WIFI_PASSWORD "Your_Password"

    secrets.h is git-ignored, so your credentials never get committed.

  3. Review config.h for anything you want to change (logging interval, default units, sensor addresses, etc.) — see Configuration.

  4. Build and upload to the ESP32. PlatformIO pulls the libraries listed in platformio.ini automatically.

  5. Open the serial monitor at 115200 baud. The board prints each sensor's init result, the assigned IP address, and how many history rows it seeded from the SD card. Browse to that IP on any device on the same network.

A healthy boot with all three sensors looks like this:

ESP32 Environmental Wifi Webserver
=====================================
Initializing sensors...
✓ BME680 initialized
SCD41 auto-self-calibration: OFF
✓ SCD41 initialized
✓ Gravity O2 initialized
Initializing SD card...
✓ Data file exists
Connecting to WiFi: homeRF
...
IP address: 192.168.5.157
Waiting for NTP time sync...
Local time: Mon Jun 29 21:20:34 2026
Seeded ring buffer with 1440 row(s) from /sensor_log.csv
Web server started
Read @ 21:21:27 | T_BME: 27.09C | T_SCD: 25.61C | CO2: 725.00 ppm | O2: 20.72 %
Data logged to SD card

Each sensor is independent — if one is absent the board prints that and keeps going with the rest.

Libraries (declared in platformio.ini as lib_deps; also available via the Arduino IDE Library Manager):

  • esp32async/ESPAsyncWebServer
  • esp32async/AsyncTCP
  • adafruit/Adafruit BME680 Library
  • adafruit/Adafruit Unified Sensor
  • sensirion/Sensirion I2C SCD4x
  • dfrobot/DFRobot_OxygenSensor

Configuration

Everything tunable lives in config.h, documented inline. Highlights:

Setting Purpose
MAIN_TITLE, SUBTITLE, BYLINE Dashboard header text (and link).
TZ_INFO, NTP_SERVER Timezone (POSIX string, auto-DST) and time source.
LOG_INTERVAL_MS Time between readings (default 60000 = 60 s).
HISTORY_MAX Readings kept in RAM for the live plot (default 1440 = 24 h).
DEFAULT_UNIT Units on load: 0 = °C/hPa, 1 = °F/mmHg, 2 = °F/PSI.
PLOT_SHADOW true for the filled "shadow" look, false for clean lines.
BME680_ADDR, SCD41_ADDR, O2_ADDR I²C addresses.
GAS_HEAT_TEMP, GAS_HEAT_TIME BME680 gas-heater profile (VOC sensing).
SCD41_AUTO_CALIBRATION Off by default — leave off for sealed-chamber work; turn on for open-room monitoring.
DO_O2_CALIBRATION, O2_CALIBRATION_VOL One-point O₂ calibration at boot.
O2_AVERAGE_COUNT Samples averaged per O₂ reading (default 10).
WARMUP_SKIP_ROWS Discard the first N readings (sensor warm-up).
CSV_FILENAME Log file name on the card.
DELETE_LOGFILE_PW Password to clear the log from the dashboard. Ships as a placeholder (admin) — change it to your own before deploying.

WiFi credentials are deliberately not here — they live in secrets.h.


Using the dashboard

The page auto-refreshes every 30 seconds. Controls:

  • Units buttons — switch temperature/pressure units live.
  • Refresh Charts — redraw immediately from the current in-RAM history (carried over from the original tutorial; the page also auto-refreshes).
  • Download CSV — download the complete on-disk log under a timestamped filename.
  • Delete All Data — clear the log file and the live buffer (password-gated).
  • Reset Zoom — return all charts to the full view after zooming/panning.

Charts appear only for sensors that are present, so the layout adapts to your hardware automatically.

Adaptive display (graceful fallback)

The dashboard reshapes itself to whatever hardware is connected — there are no broken charts or zero-valued plots for missing sensors. With no sensors it still serves the header, controls, and latest-reading line; each sensor you add brings its charts online. The full-page captures below show the progression from zero to all three sensors (PDF, opens in your browser):

CSV format

One timestamped row per reading, 14 columns. Column names follow a measurement_sensor_unit convention so each is self-describing. Absent or failed sensors record NaN (read as "missing" by MATLAB, pandas, and Excel — never a misleading 0):

temp_bme680_C, temp_bme680_F, temp_scd41_C, temp_scd41_F,
humidity_bme680_percent, humidity_scd41_percent,
pressure_bme680_hPa, pressure_bme680_mmHg, pressure_bme680_psi,
gas_bme680_kOhms, o2_sen0322_percent, co2_scd41_ppm, time, day

Each measurement is stored in every unit it could be wanted in (temperature in both °C and °F; pressure in hPa, mmHg, and PSI) so the file drops straight into a spreadsheet or analysis script with no conversion.

Worked examples for loading and plotting the log — in Python, MATLAB, and Excel, including how each handles the NaN values and reboot gaps — are in the examples/ folder, along with a real sample log to run them against.


Signal integrity (three sensors on one I²C bus)

A common worry with multiple devices on a shared I²C bus is whether the parallel breakout pull-up resistors load the bus too heavily. On this build they don't. Bench measurements (Tektronix MSO64B) confirm it: the clock runs at the ESP32's default 99.2 kHz, the SDA rise times are 315–328 ns — about a third of the 1000 ns standard-mode limit — and all three sensors decode without protocol errors.

SCD41 rise time 315 ns, with the 99.2 kHz clock spectrum

The full write-up — clock spectrum, per-sensor rise times, protocol decodes, and how to reproduce the captures — is in SIGNAL_INTEGRITY.md, with the decoded transactions saved as CSV in docs/scope-data/.


Where this is going

This is intentionally a general-purpose environmental monitor, but the architecture was built with a specialization in mind: a slow-motion gas-analysis instrument for biology and bacterial-culture metabolism, where CO₂ accumulation and O₂ depletion in a sealed chamber track the activity of a living culture over time. The seams for that fork are already in place — the config.h/secrets.h split, the per-sensor presence handling, and the SCD41_AUTO_CALIBRATION toggle (which flips the instrument between open-room monitoring and sealed-chamber experiments with a single setting). A future fork will add a syringe-pump module for timed gas sampling. If you want to build your own specialized instrument, this codebase is meant to be reconfigured rather than rewritten.


Acknowledgments

The charting and SD-logging foundation began with Rui Santos's BME280 web-server tutorial at Random Nerd Tutorials, which this project extends from a single sensor to a configurable multi-sensor instrument with adaptive display, time-axis plotting, reboot-safe history, and a publishable configuration structure.

On the human/AI development process

This project was developed through a collaboration between myself, an experienced engineer, and modern AI assistants — primarily Anthropic's Claude and to a lesser extent OpenAI's ChatGPT. It seems worthwhile to be transparent about how that collaboration actually worked.

The engineering direction and technical decisions remained human: the selection of the sensor suite and its scientific rationale, the system architecture, the data-storage model, the choice of append-only logging, timezone-aware timestamp handling, startup initialization, calibration strategy, and the overall design philosophy. Proposed implementations were reviewed, tested, and frequently revised — including finding and correcting bugs while dancing between Claude for heavy lifting and ChatGPT for creative changes in direction.

The AI assistants contributed in different but complementary ways. They accelerated implementation, recalled APIs and library details, generated code examples, reviewed designs, explained unfamiliar technologies, suggested alternative approaches, and acted as knowledgeable brainstorming partners during debugging and refinement. Rather than replacing engineering judgment, they significantly reduced development time by allowing ideas to be explored and evaluated much more rapidly.

The result reflects the strengths of both collaborators: decades of human engineering experience guiding the design, reviewing code, and validating the instrument — combined with the speed, breadth of historical and modern knowledge, and interactive problem-solving of today's AI tools.


License

MIT © 2026 Ron Fredericks / BiophysicsLab.com

About

ESP32 multi-sensor environmental monitor (BME680, SCD41, SEN0322) with microSD CSV logging and a live web dashboard — documented end to end, including I2C signal-integrity oscilloscope work, and Python/MATLAB/Excel offline plotting examples using a log file saved to your device with one click.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages