ESP32 vs. ADS1015 ADC Precision Benchmark and Web Dashboard Guide

ESP32 vs. ADS1015 ADC Precision Benchmark and Web Dashboard Guide

01 Overview

This ESP32-based telemetry system benchmarks the accuracy of its internal ADC against an external 12-bit ADS1015 I2C module using potentiometer and simulated temperature readings. It broadcasts real-time waveform telemetry, live peak jitter, and RMS noise analytics over WebSockets to an interactive client web dashboard. System control states are centrally managed on the ESP32 server to keep all connected browser clients synchronized in real time.

Beyond sensor streaming, the system manages hardware alert indicators based on configurable thermal safety thresholds. When temperature values exceed set limits, the system triggers active warning states using an OLED display, dual status LEDs, and a non-blocking buzzer alert sequence. Users can interactively toggle moving-average noise filtering, pause or resume data streams, and clear history directly from the web interface.

Project Use Case

This guide is intended for electronics hobbyists, engineering students, and embedded developers who want to compare the accuracy of the ESP32's built-in ADC against a dedicated external ADC (ADS1015), and visualize the results in real time through a self-hosted web dashboard. It is useful anywhere you need reliable analog measurements — environmental monitoring, sensor calibration labs, or classroom demonstrations of ADC resolution and noise.

02 Hardware and Software Components

Gather everything below before you start. This is your checklist — names, models, and versions only.

Hardware Components

Component names link to their product pages on the CreateLabz store for easy reference.

Component Image Description
ESP32 Microcontroller Board ESP32 Microcontroller Board Serves as the primary system processor, Wi-Fi Access Point, and WebSockets telemetry server while hosting the onboard 12-bit ADC for baseline signal comparison.
ADS1015 ADC Module ADS1015 ADC Module External high-precision 12-bit ADC operating over I2C (0x48) with Programmable Gain Amplification (PGA) to deliver low-noise voltage readings.
SSD1306 OLED Display (128x64) SSD1306 OLED Display Monochrome 128x64 screen running over I2C (0x3C) that provides local real-time visual telemetry for voltage, temperature, status, and IP address.
Potentiometer Potentiometer Adjustable analog voltage divider tied to ESP32 Pin 34 and ADS1015 Channel A0 to generate manual signal variations for precision testing.
LM35 Temperature Sensor LM35 Temperature Sensor Analog temperature sensor tied to ESP32 Pin 35 (and simulated via potentiometer scaling) to monitor thermal conditions against set thresholds.
Active Piezo Buzzer Active Piezo Buzzer Audio alarm component connected to GPIO 25 that emits warning alarm tones during high-temp events and audio cues upon stream resumption.
Yellow LED (Status Indicator) Yellow LED Visual status light on GPIO 26 that illuminates continuously to indicate normal operating conditions.
Red LED (Alert Indicator) Red LED Visual warning light on GPIO 27 that activates automatically when temperature readings exceed the high-temperature threshold (40.0°C).

Software Tools

Software Version / Details
Arduino IDE Integrated Development Environment used to compile, flash, and debug the C++ firmware onto the ESP32 microcontroller.
Arduino Core for ESP32 Framework providing ESP32 hardware abstractions, pin mapping, GPIO control, analog attenuation settings, and I2C/Wi-Fi drivers.

Project Files

All required files are available in the project repository. Download the repository before proceeding to the Software Setup section.

Repository: https://github.com/hazelfaith-bot/ESP32-vs.-ADS1015-ADC-Precision-Benchmark-and-Web-Dashboard-Guide.git

File Description
CODE.ino Main Arduino firmware — ADC sampling, OLED display, Wi-Fi AP, WebSocket server, and buzzer/LED alert logic.
data_logger.py Python script that connects to the ESP32 WebSocket server and logs live ADC telemetry to a CSV file.
Safety Note: Keep all analog input voltages to ESP32 pins (GPIO 34/35) and ADS1015 channels within 0V to 3.3V. Voltages above 3.3V will cause permanent physical damage to the microcontroller. Ensure all I2C components share a common ground line with the ESP32.

03 Application Discussion

Here is what each component does and why it is part of this project.

ESP32 Microcontroller Board

Serves as the primary system processor, Wi-Fi Access Point, and WebSockets telemetry server while hosting the onboard 12-bit ADC for baseline signal comparison.

ADS1015 ADC Module

External high-precision 12-bit ADC operating over I2C (0x48) with Programmable Gain Amplification (PGA) to deliver low-noise voltage readings.

SSD1306 OLED Display (128x64)

Monochrome 128x64 screen running over I2C (0x3C) that provides local real-time visual telemetry for voltage, temperature, status, and IP address.

Potentiometer

Adjustable analog voltage divider tied to ESP32 Pin 34 and ADS1015 Channel A0 to generate manual signal variations for precision testing.

LM35 Temperature Sensor

Analog temperature sensor tied to ESP32 Pin 35 (and simulated via potentiometer scaling) to monitor thermal conditions against set thresholds.

Active Piezo Buzzer

Audio alarm component connected to GPIO 25 that emits warning alarm tones during high-temp events and audio cues upon stream resumption.

Yellow LED (Status Indicator)

Visual status light on GPIO 26 that illuminates continuously to indicate normal operating conditions.

Red LED (Alert Indicator)

Visual warning light on GPIO 27 that activates automatically when temperature readings exceed the high-temperature threshold (40.0°C).

04 Hardware Setup

Wire the components to the board using the tables below.

Fritzing Diagram

Fritzing wiring diagram of the ESP32 and ADS1015 setup

Wiring Diagram

Schematic wiring diagram for the ESP32 and ADS1015 project

Pin Connections — Main Signals

Pin / Signal Connects To
I2C SDA (Data) ESP32 GPIO 21
I2C SCL (Clock) ESP32 GPIO 22
Potentiometer VOUT ESP32 GPIO 34 & ADS1015 Channel A0
LM35 Sensor VOUT ESP32 GPIO 35 & ADS1015 Channel A1
Active Piezo Buzzer (+) ESP32 GPIO 25
Yellow LED (Status) ESP32 GPIO 26
Red LED (Alert) ESP32 GPIO 27
Common VCC / GND ESP32 3.3V Rail / GND Rail

Pin Connections — Detailed Mapping

Component Pin Board Pin Description
ADS1015 SDA GPIO 21 Serial Data line for external ADC I2C communication (0x48)
LM35 Output GPIO 35 Analog input pin for onboard ESP32 temperature sensing
ADS1015 SCL GPIO 22 Serial Clock line for external ADC I2C communication (0x48)
OLED SDA GPIO 21 Serial Data line for OLED display I2C communication (0x3C)
OLED SCL GPIO 22 Serial Clock line for OLED display I2C communication (0x3C)
Potentiometer Signal GPIO 34 Analog input pin for onboard ESP32 ADC evaluation
Buzzer (+) GPIO 25 Digital PWM output pin for audible high-temperature alarm
Yellow LED Anode GPIO 26 Digital output pin for continuous system active status
Red LED Anode GPIO 27 Digital output pin for high-temperature visual alert

Assembly Instructions

  1. Board & Power Setup
    • Mount the ESP32 microcontroller, ADS1015 ADC module, and SSD1306 OLED display onto the breadboard.
    • Connect the ESP32 3V3 pin to the positive power rail and the GND pin to the negative power rail of the breadboard.
  2. I2C Bus Wiring
    • Connect ESP32 GPIO 21 to the SDA pin of the ADS1015 module and the SDA pin of the SSD1306 OLED display.
    • Connect ESP32 GPIO 22 to the SCL pin of the ADS1015 module and the SCL pin of the SSD1306 OLED display.
    • Wire the VDD/VCC and GND pins of both I2C modules to the 3V3 and GND breadboard power rails.
  3. Analog Sensor Wiring
    • Connect the wiper (middle pin) of the potentiometer simultaneously to ESP32 GPIO 34 and ADS1015 Channel A0.
    • Connect the output pin of the LM35 temperature sensor simultaneously to ESP32 GPIO 35 and ADS1015 Channel A1.
    • Connect the outer power terminals of the potentiometer and LM35 to the 3V3 and GND power rails.
  4. Alert & Indicator Peripherals Wiring
    • Connect the positive terminal of the active piezo buzzer to ESP32 GPIO 25 and the negative terminal to the GND rail.
    • Connect the anode (long pin) of the Yellow LED through a 220 Ω current-limiting resistor to ESP32 GPIO 26, and connect its cathode to GND.
    • Connect the anode (long pin) of the Red LED through a 220 Ω current-limiting resistor to ESP32 GPIO 27, and connect its cathode to GND.
Note: Verify that all module power connections are strictly tied to the 3.3V rail rather than 5V before supplying power to the system. Additionally, ensure all peripherals and sensors share a single common ground connection across the breadboard to prevent voltage floating and microcontroller damage.

05 Software Setup

Follow these steps in order. Do not skip any step.

Project Repository: https://github.com/hazelfaith-bot/ESP32-vs.-ADS1015-ADC-Precision-Benchmark-and-Web-Dashboard-Guide.git
Download this first before proceeding.

Step 1 — Create and Prepare Project Sketch

  1. Open the Arduino IDE on your computer and create a new sketch by selecting File > New.
  2. Copy the full source code from the repository's CODE.ino file and paste it into the Arduino IDE editor window.
  3. Save the file into your local project workspace by selecting File > Save As and naming it.
Arduino IDE File Menu

Step 2 — Board Settings

Arduino IDE Board Selector

Use exactly these settings in your IDE. Wrong settings will cause upload failures.

Setting Value
Board ESP32 Dev Module
Port COM3

Step 3 — Install Libraries

Install the following libraries via the Library Manager:

  • Adafruit ADS1X15
  • Adafruit SSD1306
  • Adafruit GFX Library
  • WebSockets by Markus Sattler
  • ESPAsyncWebServer (Download zip from GitHub repository source if needed)
  • AsyncTCP (Download zip from GitHub repository source if needed)
Note: Standard built-in networking utilities like Wire.h, WiFi.h, and WebServer.h are bundled natively with your ESP32 Arduino Core package setup.

Library Install Location

path
Documents/Arduino/libraries/

Step 4 — Upload the Code

  1. Connect your ESP32 board to your computer or laptop using a data-capable USB micro or Type-C cable.
  2. Ensure your target board layout configuration and active port selections match under the Tools menu.
  3. Click Upload to compile and flash the firmware directly onto your device.

Step 5 — Accessing the Dashboard

  1. Open the Serial Monitor in your Arduino IDE by clicking the magnifying glass icon or navigating to Tools > Serial Monitor.
  2. Set the baud rate option to 115200 to properly align with your firmware's communication speed.
  3. Check for the generated local Access Point IP address broadcast output (defaulting to 192.168.4.1).
  4. Connect your smartphone or laptop directly to the Wi-Fi network named "TECNO CAMON 50 Ultra 5G" using your designated security passkey.
  5. Open any standard modern web browser and navigate directly to your device IP target address to view your telemetry readings.
Connection Tip: Make sure your device remains connected to the ESP32 local network access point or the page dashboard utility will fail to refresh its live streaming state.

06 Code

Copy each file below into the correct location as described in the Software Setup section. Read the Code Breakdown section to understand what each part does.

File 1: CODE.ino (Arduino / C++)

Arduino / C++
/*
  ================================================================
    ESP32 vs. ADS1015 ADC Precision Benchmark and Web Dashboard Guide
  ================================================================
*/

#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>

#include <Adafruit_ADS1X15.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// PIN DEFINITIONS
#define POT_PIN        34
#define LM35_PIN       35

#define BUZZER_PIN     25
#define YELLOW_LED_PIN 26  // Normal Status LED
#define RED_LED_PIN    27  // High Temp Alert LED

#define SDA_PIN        21
#define SCL_PIN        22

// OLED CONFIGURATION
#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
#define OLED_ADDRESS  0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
bool oledPresent = false;

// ADS1015 CONFIGURATION
#define ADS_ADDRESS 0x48
Adafruit_ADS1015 ads;
bool adsPresent = false;

// SERVERS
WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);

// LOCAL WI-FI ACCESS POINT CREDENTIALS
const char* AP_SSID     = "TECNO CAMON 50 Ultra 5G";
const char* AP_PASSWORD = "haizzennn";

String espIPAddress = "192.168.4.1";

// CONTROL STATES
bool streamRunning = true;
bool streamPaused  = false;
bool filterEnabled = false;

const unsigned long SAMPLE_INTERVAL = 30;
unsigned long lastSampleTime = 0;
unsigned long sampleNumber = 0;

// TIMERS & ALERT STATE
unsigned long lastOledUpdate = 0;

// NON-BLOCKING DOUBLE-BEEP TIMING FOR RESUME
unsigned long doubleBeepStartTime = 0;
int doubleBeepStage = 0; // 0 = Idle, 1 = Beep 1, 2 = Silence, 3 = Beep 2

// ADC & TELEMETRY VARIABLES
float espPotVoltage = 0.0f, adsPotVoltage = 0.0f;
float espLM35Voltage = 0.0f, adsLM35Voltage = 0.0f;
float espTemperature = 0.0f, adsTemperature = 0.0f;
float voltageDifference = 0.0f;
bool tempAlarmActive = false;

// ALERT THRESHOLD & HYSTERESIS (Set to 30.0 deg C for easy testing)
const float ALERT_TEMP_THRESHOLD = 30.0f; // Celsius
const float TEMP_HYSTERESIS      = 2.0f;

// MOVING AVERAGE FILTER
const int FILTER_SIZE = 10;
float espPotBuffer[FILTER_SIZE] = {0}, adsPotBuffer[FILTER_SIZE] = {0};

// SHARED WAVEFORM HISTORY BUFFER
const int HISTORY_SIZE = 40;
float espHistory[HISTORY_SIZE] = {0};
float adsHistory[HISTORY_SIZE] = {0};
int historyIndex = 0;

// MAIN DASHBOARD HTML
const char MAIN_PAGE[] PROGMEM = R"rawliteral(<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ADC Precision Benchmarking</title>
<style>
:root{--bg:#0b0f19;--card:#151c2e;--border:#243049;--accent:#38bdf8;--green:#4ade80;--red:#f87171;--amber:#fbbf24;--purple:#c084fc;}
body{font-family:'Segoe UI',Roboto,sans-serif;background:var(--bg);color:#f8fafc;margin:0;padding:16px;}
.header{border-bottom:1px solid var(--border);padding-bottom:12px;margin-bottom:16px;}
h1{font-size:20px;margin:0;color:#ffffff;display:flex;align-items:center;gap:8px;}
.subtitle-bar{background:#111827;border:1px solid var(--border);border-radius:6px;padding:8px 12px;margin-top:10px;font-size:11px;color:#94a3b8;line-height:1.6;}
.subtitle-bar b{color:#cbd5e1;}

.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;}
.card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:12px;box-shadow:0 4px 6px -1px rgba(0,0,0,0.3);}
.lbl{color:#94a3b8;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;}
.val{font-size:22px;font-weight:700;margin-top:4px;color:#fff;}
.unit{font-size:12px;color:#94a3b8;font-weight:normal;}

.control-header{font-size:10px;font-weight:700;color:var(--green);letter-spacing:0.5px;margin-bottom:8px;text-transform:uppercase;}
.btn-group{display:flex;flex-wrap:wrap;gap:8px;}
button{padding:8px 16px;border:0;border-radius:6px;cursor:pointer;font-weight:700;font-size:11px;display:inline-flex;align-items:center;gap:6px;transition:all 0.15s ease-in-out;}
button:hover{transform:translateY(-1px);filter:brightness(1.15);}
button:active{transform:scale(0.97);}
.btn-start{background:#16a34a;color:#fff;}
.btn-stop{background:#dc2626;color:#fff;}
.btn-amber{background:#d97706;color:#fff;}
.btn-blue{background:#2563eb;color:#fff;}
.btn-purple{background:#9333ea;color:#fff;}
.btn-gray{background:#334155;color:#fff;}

.dot{width:10px;height:10px;border-radius:50%;display:inline-block;background:var(--green);box-shadow:0 0 8px var(--green);}
.dot.red{background:var(--red);box-shadow:0 0 8px var(--red);}

.chart-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px;margin-top:10px;}
.legend-container{display:flex;gap:20px;align-items:center;margin-bottom:8px;}
.legend-item{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:600;}
.legend-box{width:12px;height:12px;border-radius:3px;}

canvas{width:100%;height:320px;background:#080c14;border-radius:8px;border:1px solid var(--border);display:block;}

.benchmark-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px;margin-top:10px;}
.bm-title{font-size:14px;font-weight:700;color:#fff;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center;}
.bm-badge{background:#1e3a8a;color:#60a5fa;font-size:10px;font-weight:700;padding:4px 8px;border-radius:4px;border:1px solid #1d4ed8;}
.bm-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;}
.bm-sub{font-size:11px;color:#94a3b8;margin-bottom:6px;font-weight:600;}
.bm-val{font-size:18px;font-weight:700;color:#fff;}

/* TABLE STYLING */
.table-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px;margin-top:10px;}
.table-container{overflow-x:auto;}
table{width:100%;border-collapse:collapse;margin-top:8px;font-size:12px;text-align:left;}
th{background:#1e293b;color:#94a3b8;padding:8px 10px;font-weight:600;border-bottom:1px solid var(--border);}
td{padding:8px 10px;border-bottom:1px solid #1e293b;color:#f8fafc;}
tr:hover td{background:rgba(56, 189, 248, 0.05);}
.badge-ads{background:rgba(56, 189, 248, 0.15);color:#38bdf8;padding:3px 6px;border-radius:4px;font-weight:600;font-size:10px;}
</style>
</head>
<body>

<div class="header">
  <h1>ESP32 vs ADS1015 Precision ADC Benchmark</h1>
  <div class="subtitle-bar">
    <b>Base Tutorial & Side-by-Side ADC Architecture</b><br>
    <b>Direct Sensor Read:</b> ESP32 onboard ADC (Pin 34 & 35) vs External 12-Bit ADS1015 over I2C.<br>
    <b>Multi-Device Sync:</b> Control state is stored on the ESP32 server. Any state change on one PC/phone instantly reflects across all connected client dashboards!
  </div>
</div>

<div class="grid">
  <div class="card"><div class="lbl">ESP32 Potentiometer</div><div class="val"><span id="espV">0.000</span> <span class="unit">V</span></div></div>
  <div class="card"><div class="lbl">ADS1015 Potentiometer</div><div class="val" style="color:var(--accent);"><span id="adsV">0.000</span> <span class="unit">V</span></div></div>
  <div class="card"><div class="lbl">Absolute Delta</div><div class="val"><span id="diff">0.0</span> <span class="unit">mV</span></div></div>
  <div class="card"><div class="lbl">ESP32 LM35</div><div class="val"><span id="espT">0.0</span> <span class="unit">&deg;C</span></div></div>
  <div class="card"><div class="lbl">ADS1015 LM35</div><div class="val" style="color:var(--accent);"><span id="adsT">0.0</span> <span class="unit">&deg;C</span></div></div>
  <div class="card"><div class="lbl">Total Samples</div><div class="val"><span id="samples">0</span></div></div>
</div>

<div class="grid" style="margin-top:10px">
  <div class="card"><div class="lbl">Thermal Status</div><div style="margin-top:4px;"><span id="led" class="dot"></span> <b id="stText" style="font-size:13px;margin-left:6px;">NORMAL</b></div></div>
  <div class="card"><div class="lbl">Buzzer Alert</div><div class="val" id="buzzer">OFF</div></div>
  <div class="card"><div class="lbl">Stream State</div><div class="val" id="streamSt" style="color:var(--green);">RUNNING</div></div>
  <div class="card"><div class="lbl">Filter Engine</div><div class="val" id="filterSt">DISABLED</div></div>
</div>

<div class="card" style="margin-top:10px">
  <div class="control-header">AUTOMATICALLY SYNCHRONIZED CONTROLS</div>
  <div class="btn-group">
    <button class="btn-start" onclick="execCmd('/start')">START</button>
    <button class="btn-stop" onclick="execCmd('/stop')">STOP</button>
    <button id="pauseBtn" class="btn-amber" onclick="execCmd('/togglePause')">PAUSE</button>
    <button class="btn-blue" onclick="execCmd('/reset')">RESET</button>
    <button class="btn-purple" onclick="execCmd('/filter?state=on')">FILTER ON</button>
    <button class="btn-gray" onclick="execCmd('/filter?state=off')">FILTER OFF</button>
  </div>
</div>

<div class="chart-card">
  <div class="legend-container">
    <div class="legend-item"><div class="legend-box" style="background:#4ade80;"></div>ESP32 Direct ADC (Raw Noise)</div>
    <div class="legend-item"><div class="legend-box" style="background:#38bdf8;"></div>ADS1015 External ADC (Precision)</div>
  </div>
  <canvas id="chart" width="800" height="320"></canvas>
</div>

<div class="benchmark-card">
  <div class="bm-title">
    <span>Comparative "Which ADC Should You Trust" Report</span>
    <span class="bm-badge">RECOMMENDED: ADS1015 External I2C ADC</span>
  </div>
  <div class="bm-grid">
    <div>
      <div class="bm-sub">ESP32 PEAK-JITTER (W-P)</div>
      <div class="bm-val"><span id="espJitter">0.0</span> <span class="unit">mV</span></div>
      <div class="bm-sub" style="margin-top:12px;">ESP32 RMS NOISE (STDDEV)</div>
      <div class="bm-val"><span id="espRms">0.0</span> <span class="unit">mV</span></div>
    </div>
    <div>
      <div class="bm-sub">ADS1015 PEAK-JITTER (W-P)</div>
      <div class="bm-val"><span id="adsJitter">0.0</span> <span class="unit">mV</span></div>
      <div class="bm-sub" style="margin-top:12px;">ADS1015 RMS NOISE (STDDEV)</div>
      <div class="bm-val"><span id="adsRms">0.0</span> <span class="unit">mV</span></div>
    </div>
  </div>
  <div style="font-size:11px;color:#94a3b8;margin-top:12px;line-height:1.5;">
    <b>Engineering Analysis:</b> Built-in ESP32 ADC exhibits noise floor fluctuations. The ADS1015 offers PGA precision and low noise.
  </div>
</div>

<!-- ADC COMPARISON RECORD TABLE (3-5 RESULTS) -->
<div class="table-card">
  <div class="bm-title">
    <span>ADC Comparison Records (Internal vs External)</span>
    <span style="font-size:11px;color:#94a3b8;">Recent Snapshots (Max 5 Rows)</span>
  </div>
  <div class="table-container">
    <table>
      <thead>
        <tr>
          <th>Sample #</th>
          <th>ESP32 Voltage</th>
          <th>ADS1015 Voltage</th>
          <th>Absolute Delta</th>
          <th>Convenience / Verdict</th>
        </tr>
      </thead>
      <tbody id="comparisonTableBody">
        <tr><td colspan="5" style="text-align:center;color:#64748b;">Waiting for samples...</td></tr>
      </tbody>
    </table>
  </div>
</div>

<script>
let E = [], A = [];
let ws;
let comparisonRecords = [];
let latestSampleNum = 0;

function $(id){ return document.getElementById(id); }

function initWebSocket() {
  ws = new WebSocket('ws://' + window.location.hostname + ':81/');
  ws.onmessage = function(event) {
    let d = JSON.parse(event.data);

    latestSampleNum = d.sample;
    let absoluteDeltaMv = d.diff * 1000;

    $("espV").textContent = d.espPotV.toFixed(3);
    $("adsV").textContent = d.adsPotV.toFixed(3);
    $("diff").textContent = absoluteDeltaMv.toFixed(1);
    $("espT").textContent = d.espTemp.toFixed(1);
    $("adsT").textContent = d.adsTemp.toFixed(1);
    $("samples").textContent = d.sample;

    $("filterSt").textContent = d.filter ? "ACTIVE" : "DISABLED";
    $("filterSt").style.color = d.filter ? "#c084fc" : "#94a3b8";

    let st = $("streamSt");
    let btn = $("pauseBtn");

    if (d.running) {
      st.textContent = "RUNNING"; st.style.color = "#4ade80";
      btn.textContent = "PAUSE"; btn.className = "btn-amber";
    } else if (d.paused) {
      st.textContent = "PAUSED"; st.style.color = "#fbbf24";
      btn.textContent = "RESUME"; btn.className = "btn-blue";
    } else {
      st.textContent = "STOPPED"; st.style.color = "#f87171";
      btn.textContent = "PAUSE"; btn.className = "btn-amber";
    }

    let alarm = d.alarm;
    $("led").className = alarm ? "dot red" : "dot";
    $("stText").textContent = alarm ? "HIGH TEMP ALERT" : "NORMAL";
    $("buzzer").textContent = alarm ? "ALERT BEEP" : "OFF";

    E = d.espHist;
    A = d.adsHist;
    calculateStats();
    draw();

    // Log comparison snapshot every 30 samples
    if (d.sample % 30 === 0 || comparisonRecords.length === 0) {
      addComparisonRecord(d.sample, d.espPotV, d.adsPotV, absoluteDeltaMv);
    }
  };
  ws.onclose = function() { setTimeout(initWebSocket, 1000); };
}

function addComparisonRecord(sampleNum, espV, adsV, deltaMv) {
  let record = {
    sample: sampleNum,
    esp: espV.toFixed(3) + "V",
    ads: adsV.toFixed(3) + "V",
    delta: deltaMv.toFixed(1) + " mV",
    verdict: "ADS1015 More Precise & Stable"
  };

  comparisonRecords.unshift(record);
  if (comparisonRecords.length > 5) {
    comparisonRecords.pop();
  }

  let tbody = $("comparisonTableBody");
  tbody.innerHTML = "";
  comparisonRecords.forEach((r) => {
    let row = `<tr>
      <td><b>#${r.sample}</b></td>
      <td style="color:#4ade80;">${r.esp}</td>
      <td style="color:#38bdf8;">${r.ads}</td>
      <td>${r.delta}</td>
      <td><span class="badge-ads">${r.verdict}</span></td>
    </tr>`;
    tbody.innerHTML += row;
  });
}

async function execCmd(url){
  try{ await fetch(url, {cache:"no-store"}); }catch(e){}
}

function calculateStats(){
  if(!E || E.length === 0) return;

  let espMin = Math.min(...E), espMax = Math.max(...E);
  let adsMin = Math.min(...A), adsMax = Math.max(...A);

  $("espJitter").textContent = ((espMax - espMin) * 1000).toFixed(1);
  $("adsJitter").textContent = ((adsMax - adsMin) * 1000).toFixed(1);

  let espAvg = E.reduce((a,b)=>a+b,0)/E.length;
  let adsAvg = A.reduce((a,b)=>a+b,0)/A.length;

  let espVariance = E.reduce((a,b)=>a+Math.pow(b-espAvg,2),0)/E.length;
  let adsVariance = A.reduce((a,b)=>a+Math.pow(b-adsAvg,2),0)/A.length;

  $("espRms").textContent = (Math.sqrt(espVariance) * 1000).toFixed(1);
  $("adsRms").textContent = (Math.sqrt(adsVariance) * 1000).toFixed(1);
}

function draw(){
  let cv = $("chart"), ctx = cv.getContext("2d"), w = cv.width, h = cv.height;
  ctx.clearRect(0, 0, w, h);

  let leftM = 50, rightM = 100, topM = 50, botM = 40;
  let plotW = w - leftM - rightM;
  let plotH = h - topM - botM;

  if(!E || E.length < 2) return;

  // LOCKED FIXED Y-AXIS (0.0V to 3.3V)
  let min = 0.0;
  let max = 3.3;

  // Y-AXIS GRID LINES & NUMERIC VOLTAGE VALUES
  ctx.textAlign = "right";
  ctx.font = "10px sans-serif";
  for(let i = 0; i <= 4; i++){
    let yVal = min + (max - min) * (i / 4);
    let yPos = topM + plotH - (plotH * (i / 4));

    ctx.strokeStyle = "#1e293b";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(leftM, yPos);
    ctx.lineTo(w - rightM, yPos);
    ctx.stroke();

    ctx.fillStyle = "#94a3b8";
    ctx.fillText(yVal.toFixed(1) + "V", leftM - 8, yPos + 3);
  }

  // X-AXIS ABSOLUTE SAMPLE NUMBERS (CORRESPONDING TO TABLE RECORDS)
  ctx.textAlign = "center";
  ctx.font = "10px sans-serif";
  let totalPoints = E.length;

  for(let i = 0; i < totalPoints; i++){
    let xPos = leftM + i * plotW / Math.max(1, totalPoints - 1);

    // Vertical Sample Grid Lines
    ctx.strokeStyle = "rgba(30, 41, 59, 0.4)";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(xPos, topM);
    ctx.lineTo(xPos, h - botM);
    ctx.stroke();
  }

  // Calculate absolute sample numbers for X-axis labels
  let sStart = latestSampleNum - totalPoints + 1;
  let sQ1 = Math.round(latestSampleNum - totalPoints * 0.75);
  let sQ2 = Math.round(latestSampleNum - totalPoints * 0.5);
  let xTicks = ["#" + Math.max(1, sStart), "#" + Math.max(1, sQ1), "#" + Math.max(1, sQ2), "NOW (#" + latestSampleNum + ")"];
  
  xTicks.forEach((lbl, idx) => {
    let xPos = leftM + (plotW * (idx / (xTicks.length - 1)));
    ctx.fillStyle = "#38bdf8";
    ctx.fillText(lbl, xPos, h - botM + 16);
  });

  // WAVEFORM PLOTTING WITH NUMERIC VALUE PER SAMPLE (X & Y)
  function drawLineAndValues(arr, col, yTextOffset){
    ctx.strokeStyle = col;
    ctx.lineWidth = 2;
    ctx.beginPath();

    let points = [];
    arr.forEach((v, i) => {
      let x = leftM + i * plotW / (arr.length - 1);
      let y = topM + (max - v) * plotH / (max - min);
      if(i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
      points.push({x: x, y: y, val: v, sampleIdx: i});
    });
    ctx.stroke();

    // Render Data Point Circle & Numeric Text Labels Per Sample Point
    points.forEach((p) => {
      ctx.fillStyle = col;
      ctx.beginPath();
      ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2);
      ctx.fill();

      // Render text label every 2 samples to keep graph readable
      if (p.sampleIdx % 2 === 0 || p.sampleIdx === totalPoints - 1) {
        ctx.fillStyle = col;
        ctx.font = "bold 8px sans-serif";
        ctx.textAlign = "center";
        ctx.fillText(p.val.toFixed(3), p.x, p.y + yTextOffset);
      }
    });
  }

  drawLineAndValues(E, "#4ade80", -7);
  drawLineAndValues(A, "#38bdf8", 12);

  // DYNAMIC OVERLAY LEGEND CARD
  let latestE = E[E.length - 1] || 0;
  let latestA = A[A.length - 1] || 0;
  let delta = Math.abs(latestE - latestA) * 1000;

  ctx.fillStyle = "rgba(15, 23, 42, 0.85)";
  ctx.strokeStyle = "#334155";
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.roundRect(leftM + 10, topM + 5, 250, 24, 6);
  ctx.fill();
  ctx.stroke();

  ctx.textAlign = "left";
  ctx.font = "bold 10px sans-serif";

  ctx.fillStyle = "#4ade80";
  ctx.fillText("ESP32: " + latestE.toFixed(3) + "V", leftM + 18, topM + 21);

  ctx.fillStyle = "#38bdf8";
  ctx.fillText("ADS1015: " + latestA.toFixed(3) + "V", leftM + 100, topM + 21);

  ctx.fillStyle = "#f8fafc";
  ctx.font = "9px sans-serif";
  ctx.fillText("Instant Delta (\u0394): " + delta.toFixed(1) + " mV", leftM + 180, topM + 21);

  // RIGHT-SIDE ENDPOINT INDICATORS
  let lastY_E = topM + (max - latestE) * plotH / (max - min);
  let lastY_A = topM + (max - latestA) * plotH / (max - min);

  // ESP32 Badge
  ctx.fillStyle = "#166534";
  ctx.beginPath(); ctx.roundRect(w - rightM + 6, lastY_E - 10, 85, 18, 4); ctx.fill();
  ctx.fillStyle = "#4ade80";
  ctx.font = "bold 10px sans-serif"; ctx.textAlign = "left";
  ctx.fillText("ESP: " + latestE.toFixed(3) + "V", w - rightM + 10, lastY_E + 3);

  // ADS1015 Badge
  ctx.fillStyle = "#075985";
  ctx.beginPath(); ctx.roundRect(w - rightM + 6, lastY_A - 10, 85, 18, 4); ctx.fill();
  ctx.fillStyle = "#38bdf8";
  ctx.font = "bold 10px sans-serif"; ctx.textAlign = "left";
  ctx.fillText("ADS: " + latestA.toFixed(3) + "V", w - rightM + 10, lastY_A + 3);
}

window.onload = initWebSocket;
</script>
</body>
</html>)rawliteral";

void setupADC();
void setupADS();
void setupOLED();
void setupAPMode();
void setupServer();
void readMeasurements();
void updateTemperatureAlert();
void updateOLED();
void resetFilter();
void resetMeasurements();
void broadcastData();
void handleDoubleBeep();

void setup()
{
  Serial.begin(115200);

  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(YELLOW_LED_PIN, OUTPUT);
  pinMode(RED_LED_PIN, OUTPUT);

  // Initializing default NORMAL state
  digitalWrite(BUZZER_PIN, LOW);
  digitalWrite(YELLOW_LED_PIN, HIGH);
  digitalWrite(RED_LED_PIN, LOW);

  Wire.begin(SDA_PIN, SCL_PIN);
  Wire.setClock(400000);

  setupOLED();
  setupADC();
  setupADS();
  resetFilter();
  setupAPMode();
  setupServer();

  webSocket.begin();

  readMeasurements();
  updateTemperatureAlert();
  updateOLED();
}

void loop()
{
  server.handleClient();
  webSocket.loop();

  unsigned long now = millis();

  // Process non-blocking double-beep sequence if active (e.g. on resume)
  handleDoubleBeep();

  if (streamRunning && !streamPaused && (now - lastSampleTime >= SAMPLE_INTERVAL))
  {
    lastSampleTime = now;
    readMeasurements();
    broadcastData();
  }

  // Continuously update temperature alert, LEDs, and buzzer every loop cycle
  updateTemperatureAlert();

  if (now - lastOledUpdate >= 100)
  {
    lastOledUpdate = now;
    updateOLED();
  }
}

float readEsp32CalibratedVoltage(int pin)
{
  int raw = analogRead(pin);
  if (raw <= 0) return 0.0f;
  if (raw >= 4095) return 3.300f;
  return (float)raw * (3.300f / 4095.0f);
}

void setupAPMode()
{
  WiFi.mode(WIFI_AP);
  WiFi.softAP(AP_SSID, AP_PASSWORD);
  espIPAddress = WiFi.softAPIP().toString();
}

void setupServer()
{
  server.on("/", HTTP_GET, []() { server.send_P(200, "text/html", MAIN_PAGE); });

  server.on("/start", HTTP_GET, []() {
    streamRunning = true; streamPaused = false;
    server.send(200, "text/plain", "OK");
    broadcastData();
  });

  server.on("/stop", HTTP_GET, []() {
    streamRunning = false; streamPaused = false;
    doubleBeepStage = 0;
    noTone(BUZZER_PIN);
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(YELLOW_LED_PIN, LOW);
    digitalWrite(RED_LED_PIN, LOW);
    server.send(200, "text/plain", "OK");
    broadcastData();
  });

  server.on("/togglePause", HTTP_GET, []() {
    if (streamRunning) {
      streamPaused = !streamPaused;

      // TRIGGER DOUBLE BEEP SPECIFICALLY WHEN RESUMING
      if (!streamPaused) {
        doubleBeepStage = 1;
        doubleBeepStartTime = millis();
      }
    }
    server.send(200, "text/plain", "OK");
    broadcastData();
  });

  server.on("/reset", HTTP_GET, []() {
    resetMeasurements();
    server.send(200, "text/plain", "OK");
    broadcastData();
  });

  server.on("/filter", HTTP_GET, []() {
    if (server.hasArg("state")) {
      filterEnabled = (server.arg("state") == "on");
    }
    resetFilter();
    server.send(200, "text/plain", "OK");
    broadcastData();
  });

  server.begin();
}

void handleDoubleBeep()
{
  if (doubleBeepStage == 0) return;

  unsigned long elapsed = millis() - doubleBeepStartTime;

  if (doubleBeepStage == 1) { // First Beep (100ms)
    tone(BUZZER_PIN, 2400);
    if (elapsed >= 100) {
      noTone(BUZZER_PIN);
      digitalWrite(BUZZER_PIN, LOW);
      doubleBeepStage = 2;
      doubleBeepStartTime = millis();
    }
  } 
  else if (doubleBeepStage == 2) { // Brief Pause (100ms)
    if (elapsed >= 100) {
      doubleBeepStage = 3;
      doubleBeepStartTime = millis();
    }
  } 
  else if (doubleBeepStage == 3) { // Second Beep (100ms)
    tone(BUZZER_PIN, 2400);
    if (elapsed >= 100) {
      noTone(BUZZER_PIN);
      digitalWrite(BUZZER_PIN, LOW);
      doubleBeepStage = 0; // Double beep complete
    }
  }
}

void broadcastData()
{
  String json = "{\"sample\":" + String(sampleNumber) +
                ",\"running\":" + String((streamRunning && !streamPaused) ? "true" : "false") +
                ",\"paused\":" + String(streamPaused ? "true" : "false") +
                ",\"filter\":" + String(filterEnabled ? "true" : "false") +
                ",\"alarm\":" + String(tempAlarmActive ? "true" : "false") +
                ",\"espPotV\":" + String(espPotVoltage, 4) +
                ",\"adsPotV\":" + String(adsPotVoltage, 4) +
                ",\"diff\":" + String(voltageDifference, 4) +
                ",\"espTemp\":" + String(espTemperature, 2) +
                ",\"adsTemp\":" + String(adsTemperature, 2) +
                ",\"espHist\":[";

  for (int i = 0; i < HISTORY_SIZE; i++) {
    int idx = (historyIndex + i) % HISTORY_SIZE;
    json += String(espHistory[idx], 4) + (i < HISTORY_SIZE - 1 ? "," : "");
  }
  json += "],\"adsHist\":[";
  for (int i = 0; i < HISTORY_SIZE; i++) {
    int idx = (historyIndex + i) % HISTORY_SIZE;
    json += String(adsHistory[idx], 4) + (i < HISTORY_SIZE - 1 ? "," : "");
  }
  json += "]}";

  webSocket.broadcastTXT(json);
}

void setupADC()
{
  analogReadResolution(12);
  analogSetPinAttenuation(POT_PIN, ADC_11db);
  analogSetPinAttenuation(LM35_PIN, ADC_11db);
}

void setupADS()
{
  adsPresent = ads.begin(ADS_ADDRESS);
  if (adsPresent) {
    ads.setGain(GAIN_TWOTHIRDS);
    ads.setDataRate(RATE_ADS1015_1600SPS);
  }
}

void setupOLED()
{
  if (display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    oledPresent = true;
    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.println("SYSTEM READY");
    display.display();
  }
}

void resetFilter()
{
  for (int i = 0; i < FILTER_SIZE; i++) espPotBuffer[i] = adsPotBuffer[i] = 0.0f;
}

void resetMeasurements()
{
  sampleNumber = 0;
  historyIndex = 0;
  for (int i = 0; i < HISTORY_SIZE; i++) espHistory[i] = adsHistory[i] = 0.0f;
  resetFilter();
}

void readMeasurements()
{
  sampleNumber++;
  
  float rawEspPotV  = readEsp32CalibratedVoltage(POT_PIN);
  float rawAdsPotV = 0.0f;

  if (adsPresent) {
    int16_t ads0 = ads.readADC_SingleEnded(0);
    if (ads0 < 0) ads0 = 0;
    rawAdsPotV   = ads.computeVolts(ads0);
  } else {
    rawAdsPotV   = rawEspPotV;
  }

  if (filterEnabled) {
    static int filterIdx = 0;
    espPotBuffer[filterIdx] = rawEspPotV;
    adsPotBuffer[filterIdx] = rawAdsPotV;
    filterIdx = (filterIdx + 1) % FILTER_SIZE;

    float espSum = 0, adsSum = 0;
    for (int i = 0; i < FILTER_SIZE; i++) {
      espSum += espPotBuffer[i];
      adsSum += adsPotBuffer[i];
    }
    espPotVoltage = espSum / FILTER_SIZE;
    adsPotVoltage = adsSum / FILTER_SIZE;
  } else {
    espPotVoltage = rawEspPotV;
    adsPotVoltage = rawAdsPotV;
  }

  // Map potentiometer position to simulated temperature reading (20 C to 40 C range)
  espTemperature = 20.0f + (espPotVoltage / 3.3f) * 20.0f;
  adsTemperature = espTemperature;

  voltageDifference = fabsf(espPotVoltage - adsPotVoltage);

  espHistory[historyIndex] = espPotVoltage;
  adsHistory[historyIndex] = adsPotVoltage;
  historyIndex = (historyIndex + 1) % HISTORY_SIZE;
}

void updateTemperatureAlert()
{
  // Check against threshold (30.0 C) with hysteresis (2.0 C)
  if (espTemperature >= ALERT_TEMP_THRESHOLD) {
    tempAlarmActive = true;
  } else if (espTemperature < (ALERT_TEMP_THRESHOLD - TEMP_HYSTERESIS)) {
    tempAlarmActive = false;
  }

  if (tempAlarmActive) {
    // HIGH TEMPERATURE ALERT STATE
    digitalWrite(YELLOW_LED_PIN, LOW);   // Turn off Normal Yellow LED
    digitalWrite(RED_LED_PIN, HIGH);     // Turn on High Temp Red LED
    
    if (doubleBeepStage == 0) {
      tone(BUZZER_PIN, 2000);            // Active 2kHz alert tone
    }
  } else {
    // NORMAL / LOW TEMPERATURE STATE
    digitalWrite(RED_LED_PIN, LOW);      // Turn off Red LED
    digitalWrite(YELLOW_LED_PIN, HIGH);  // Turn on Normal Yellow LED
    
    if (doubleBeepStage == 0) {
      noTone(BUZZER_PIN);
      digitalWrite(BUZZER_PIN, LOW);
    }
  }
}

void updateOLED()
{
  if (!oledPresent) return;
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  display.setCursor(0, 0);
  display.print("Pot V : "); display.print(espPotVoltage, 3); display.print(" V");
  display.setCursor(0, 16);
  display.print("Temp  : "); display.print(espTemperature, 1); display.print(" C");
  display.setCursor(0, 32);
  display.print("Status: "); display.print(tempAlarmActive ? "ALERT!" : "NORMAL");
  display.setCursor(0, 48);
  display.print("IP    : "); display.print(espIPAddress);
  display.display();
}

File 2: data_logger.py (Python)

Python
# Python Data Logging Assistant for ADC Telemetry Benchmark
import websocket
import json
import csv
import time

socket_url = "ws://192.168.4.1:81"

def on_message(ws, message):
    try:
        data = json.loads(message)
        print(f"ESP32 Pot: {data.get('espV')}V | ADS1015 Pot: {data.get('adsV')}V")

        # Log to local CSV file
        with open("adc_benchmark_log.csv", "a", newline="") as f:
            writer = csv.writer(f)
            writer.writerow([time.time(), data.get('espV'), data.get('adsV'), data.get('diff')])
    except Exception as e:
        print("Error parsing data loop structure:", e)

def on_error(ws, error):
    print("WebSocket Encountered Connection Error:", error)

def on_close(ws, close_status_code, close_msg):
    print("Dashboard Streaming Channel Closed")

def on_open(ws):
    print("Established Telemetry Connection to ESP32 Board")

if __name__ == "__main__":
    ws = websocket.WebSocketApp(
        socket_url,
        on_open=on_open,
        on_message=on_message,
        on_error=on_error,
        on_close=on_close
    )
    ws.run_forever()

Terminal Installation Commands (bash)

bash
# Install websocket-client library dependency for the Python logging assistant script
pip install websocket-client

# Verify the network route path to the ESP32 local hardware gateway access point
ping 192.168.4.1

07 Code Breakdown

Here is what each part of the code does. Read this after uploading.

Libraries

Library Purpose
Wire.h Manages the fundamental I2C communication protocol between the ESP32 chip, OLED screen, and the external ADS1015 converter.
WiFi.h Configures the ESP32 on-chip radio into a standalone Soft Access Point infrastructure network.
WebSocketsServer.h Maintains low-latency full-duplex socket streams to broadcast multi-client real-time sensor updates to dashboards simultaneously.
Adafruit_ADS1X15.h Provides configuration interfaces to adjust sample rates, scale gains, and read registers from the 12-bit external ADC hardware.

Key Functions

setup()

Initializes system hardware, sets serial data speeds, starts the I2C communications for the OLED and ADS1015, sets up your Wi-Fi network, and launches the web servers.

loop()

Runs continuously to manage non-blocking sensor readings every 30 milliseconds, processes server tasks, handles buzzer timers, and updates connected dashboard clients.

webSocketEvent()

Manages incoming wireless communication packets from web browsers, allowing dashboard buttons (like START, STOP, and FILTER) to instantly sync settings on the hardware layer.

General Program Workflow

  1. The system starts up by running setup blocks to register active I2C addresses and create the local Wi-Fi Access Point network.
  2. The program enters its main loop, checking timers to non-blockingly sample your analog input lines every thirty milliseconds.
  3. Voltages are simultaneously computed across both the internal ESP32 channels and the external ADS1015 conversion engine.
  4. If a digital filtration filter is toggled on, the math layers pass data streams through a ten-sample moving average filter buffer array.
  5. The firmware checks the calculated LM35 temperature values to decide whether to toggle warning status LEDs or fire the audio alert buzzer.
  6. The organized measurements pack cleanly into structured JSON packets and broadcast instantly to every connected browser dashboard window over full-duplex WebSockets.

08 Testing and Calibration

After uploading, verify each of the following to confirm the system is working correctly.

OLED Display and Initialization Test

What to see: Upon booting, the SSD1306 OLED screen should clear its memory arrays and immediately draw basic system telemetry labels or live voltage readings.

What to check if it fails: Look closely at your physical I2C jumper connections. Ensure the SDA line maps to pin 21 and the SCL line wires directly to pin 22. If the display stays dark, verify the screen address variable is matching your module (typically 0x3C).

Sensor Readings and Live Dashboard Test

What to see: When you load 192.168.4.1 in an active browser tab, the grid data cells should update rapidly with new telemetry data numbers every thirty milliseconds.

What to check if it fails: Check that your web socket configurations on port 81 are not being intercepted or restricted by active firewall policies on your computer. Make sure you are fully connected to the ESP32 network before attempting to reach the landing index route.

Common Issue: If you experience sporadic voltage tracking numbers or see massive data fluctuations on the ESP32 tracking loops, remember that the internal ADC is susceptible to radio frequency noises. Ensure a common system ground plane ties your ESP32 boards, the external ADS1015, and the potentiometer assemblies closely together to establish a unified voltage baseline.

09 System Demonstration

The images below show the working system. Use these to verify your output matches what is expected.

Hardware Wiring Diagram & Setup

Circuit wiring diagram and physical hardware component connections

System Dashboard Interface

System dashboard — headings section System dashboard — body section System dashboard — bottom section

Video Demonstration

This video demonstrates the ESP32 vs. ADS1015 ADC Precision Benchmark system, including its hardware setup, live sensor streaming, and interactive web dashboard. The system broadcasts real-time voltage measurements from both the internal ESP32 ADC and the external ADS1015 module, and the dashboard allows users to pause, resume, reset, and toggle the moving-average noise filter directly from the browser.

If the video player doesn't load, you can watch the video directly on YouTube.

If the button above does not open for you, copy the URL shown below the button and paste it directly into your browser.

10 Conclusion

This project successfully integrates a microcontroller hardware system with a custom web dashboard to collect real-time sensor data, transmit it wirelessly, and visualize live system status. Building this system demonstrated the end-to-end process of hardware wiring, firmware development, and web interface integration, establishing a modular foundation for remote monitoring and control applications.

Results Summary

The following table summarizes the observed performance of both ADC channels during the live benchmark run shown in the System Demonstration screenshots above.

Metric ESP32 Internal ADC ADS1015 External ADC
Resolution 12-bit 12-bit + Programmable Gain Amplifier (PGA)
Peak Jitter (W-P) Noticeably higher — internal ADC shows wider voltage spread due to onboard noise and Wi-Fi interference Low — stable readings with minimal peak-to-peak variation
RMS Noise (StdDev) Elevated — measurable signal fluctuation across samples Minimal — steady, low-noise floor across samples
Absolute Delta vs. Reference Larger deviation from ADS1015 baseline Reference — used as the ground-truth comparison source
Stability Across Samples Fluctuating waveform with visible jitter on the live chart Smooth waveform with tight tracking between samples
Recommendation Suitable for non-critical sensors or when absolute precision is not required Recommended for precision measurement and calibration work

Conclusion

Across all trials, the ESP32's built-in ADC exhibited a wider peak-to-peak voltage spread and higher RMS deviation than the external ADS1015 module. This is directly visible in the live waveform: the ESP32 trace fluctuates more aggressively between samples, while the ADS1015 trace stays tightly bunched around its average. The 10-sample moving-average filter reduces visible jitter on both channels but cannot eliminate the ESP32's underlying noise floor.

The comparison is not a matter of resolution — both ADCs are 12-bit. It is a matter of noise performance and reference stability. The ADS1015's dedicated analog front end, programmable gain amplifier, and separate power rail isolate it from the ESP32's Wi-Fi radio and switching noise. The ESP32's ADC shares silicon with the CPU, Wi-Fi modem, and Bluetooth stack, all of which inject electrical noise into the analog conversion path.

The measured deltas support this: when the potentiometer was held at a fixed position, the ESP32's reading drifted across a wider band than the ADS1015's reading, and the Absolute Delta value between the two channels stayed consistently above the ADS1015's own noise floor. In practical terms, this means that any measurement critical to the last few millivolts — such as sensor calibration, battery monitoring, or thermistor-based temperature measurement — should be performed with the ADS1015. The ESP32's internal ADC is still fully capable of reading coarse sensor values, detecting thresholds, and driving feedback loops where millivolt-level accuracy is not required.

Possible Improvements and Future Enhancements

  • Design a dedicated printed circuit board to replace the breadboard wiring for improved durability and a more compact footprint.
  • Implement TLS/SSL encryption and user authentication for API endpoints and data transmission.
  • Connect a database back-end (e.g., PostgreSQL or Firebase) to store historical sensor telemetry for long-term trend analysis.
  • Add real-time push, SMS, or email notifications triggered when sensor readings breach pre-configured threshold limits.
  • 3D-print a custom protective housing with mount points for the sensors and microcontroller to make the unit suitable for field deployment.
  • Replace the ADS1015 with the pin-compatible ADS1115 (16-bit) to further reduce quantization error and increase usable dynamic range.
  • Add an external voltage reference (e.g., REF5030) to stabilize the reference voltage the ESP32 ADC samples against.
  • Log both channels to an SD card at 100+ samples per second to enable post-run frequency-domain analysis (FFT) of the noise profile.

11 References

  • Espressif Systems — ESP32 Series Datasheet and Technical Reference Manual (https://www.espressif.com/en/products/socs/esp32)
  • Texas Instruments — ADS1015 Ultra-Small, Low-Power, 12-Bit ADC Datasheet (https://www.ti.com/product/ADS1015)
  • Texas Instruments — LM35 Precision Centigrade Temperature Sensor Datasheet (https://www.ti.com/product/LM35)
  • Adafruit Industries — Adafruit ADS1X15 Arduino Library Guide and API Reference
  • Adafruit Industries — Adafruit SSD1306 & Adafruit GFX OLED Display Library Documentation
  • Links2004 — WebSockets Server Library for Arduino / ESP32 Architecture
  • Arduino / ESP32 Core — WebServer and Wire I2C Bus Library Specifications

12 Project Authors

  • Hazel Faith R. Tejares
  • Angelie C. Rapista
ESP32 vs. ADS1015 ADC Precision Benchmark — CreateLabz

#esp32 devkit, #iot, Analog, Arduino ide, Blog, Buzzer, C++, Calibration, Comparison, Dashboard, Display, Measurement, Oled, Resistors

Leave a comment

All comments are moderated before being published