Table of Contents
Introduction
Color measurement is an important aspect of many industries, such as water testing, food production, medicine, and environmental monitoring, to name a few. A high-quality colorimeter or spectrophotometer can cost hundreds to thousands of dollars, making it difficult for hobbyists, students, and small-scale researchers to afford such equipment for their projects. This tutorial will walk you through the process of making your own WiFi-enabled colorimeter with the state-of-the-art AS7265x spectroscopy sensor, enabling you to perform high-quality color measurements at a fraction of the cost of commercial equipment.
The AS7265x spectral sensor is an advanced sensor that takes the field of miniaturized spectroscopy to the next level. Unlike regular color sensors that can only detect three bands of light, namely, red, green, and blue, the AS7265x sensor is capable of detecting 18 individual wavelength bands, ranging from the ultraviolet to the near-infrared region of the spectrum. This makes the DIY project you will build in the coming sections a powerful analytical tool that can detect chemical compositions, concentrations, and even minute differences in color that would go undetected by regular RGB sensors.
What sets this project apart from other DIY colorimeter tutorials is the integration of a web-based interface through the ESP32‘s built-in WiFi capabilities. Rather than relying on serial monitor readings or external displays, you will create a fully functional web server that displays real-time spectral data in an intuitive graphical format accessible from any device on your network. This feature makes the device practical for field work, classroom demonstrations, and remote monitoring applications where connecting to a computer is impractical or inconvenient.
What is a Colorimeter and How Does It Work?
A colorimeter is an analytical instrument designed to measure the absorption of specific wavelengths of light by a solution. The fundamental principle behind colorimetry is the Beer-Lambert law, which states that the absorbance of light through a solution is directly proportional to the concentration of the absorbing substance and the path length through the solution. This relationship enables scientists and engineers to determine unknown concentrations by comparing light absorption against known standards, making colorimeters invaluable tools in chemistry, biology, and environmental science.
Traditional colorimeters operate by passing a beam of light through a filter to select a specific wavelength, then directing that filtered light through a sample cuvette containing the solution under test. A photodetector on the opposite side measures the intensity of transmitted light, and the instrument calculates absorbance by comparing the transmitted intensity to a reference measurement. This straightforward approach has served scientists well for decades, but it suffers from limitations in wavelength selection and the inability to capture full spectral information.
The AS7265x sensor modernizes this classical approach by incorporating multiple narrow-band filters directly on the silicon substrate, essentially creating 18 separate color channels in a single chip. This design eliminates the need for mechanical filter wheels and enables simultaneous measurement across the entire spectral range in seconds rather than minutes. The result is a faster, more reliable, and more versatile instrument that can characterize samples with unprecedented detail for its size and cost.
Understanding the AS7265x Spectroscopy Sensor

The AS7265x is actually a family of three related sensors: the AS72651, AS72652, and AS72653. Together, these three chips form a complete spectral recognition system capable of measuring light intensity across 18 wavelengths. The AS72651 covers the visible spectrum from approximately 410nm to 610nm, the AS72652 extends coverage into the near-infrared region from 560nm to 860nm, and the AS72653 provides ultraviolet and blue-violet measurements from 380nm to 530nm. When combined, these sensors deliver comprehensive spectral coverage from 380nm to 860nm with minimal gaps between channels.
Key Technical Specifications
| Parameter | Specification |
| Spectral Range | 380nm to 860nm (UV to NIR) |
| Number of Channels | 18 spectral channels (6 per sensor) |
| Channel Bandwidth | 20nm full-width half-maximum (FWHM) |
| Interface | I2C (Qwiic compatible) and UART |
| Operating Voltage | 2.7V to 3.6V |
| Current Consumption | Approximately 1.2mA (measurement mode) |
| Integration Time | 2.8ms to 714ms (adjustable) |
| Built-in Illumination | LED (broadband white) and IR LED |
| I2C Address | 0x49 (AS72651), 0x4A (AS72652), 0x4B (AS72653) |
Wavelength Channel Distribution

Understanding the precise wavelength distribution of each channel helps you select the most appropriate measurements for your specific application. The AS72651 (visible sensor) provides channels at 410nm (violet), 435nm (indigo), 460nm (blue), 485nm (cyan), 510nm (green), and 550nm (yellow-green). The AS72652 (NIR sensor) adds channels at 560nm, 585nm, 645nm, 680nm, 705nm, and 860nm, covering the orange, red, and near-infrared regions. Finally, the AS72653 (UV/Violet sensor) contributes channels at 380nm (UV-A), 420nm (violet), 470nm (blue), 515nm (green), 545nm (yellow-green), and 580nm (orange-yellow), ensuring complete coverage with overlapping channels for verification purposes.
Official Sparkfun datasheet, and library : here
Why Choose the AS7265x for Your DIY Colorimeter?
Several compelling advantages make the AS7265x an ideal choice for building a homemade colorimeter:
- Factory Calibration: Each sensor undergoes individual calibration at the manufacturing facility, eliminating the need for complex calibration procedures and ensuring consistent, repeatable measurements across different units.
- Digital Output: The sensor provides direct digital readings rather than analog voltages, reducing noise susceptibility and simplifying interfacing with microcontrollers like the ESP32.
- Compact Form Factor: The small footprint allows integration into portable devices and handheld instruments, making your colorimeter practical for field applications.
- Low Power Requirements: The minimal current draw enables battery-powered operation for extended periods, essential for portable and remote monitoring applications.
- Multiple Measurement Modes: The sensor supports single-shot, continuous, and one-shot measurement modes, providing flexibility for different sampling requirements and power constraints.
Required Components and Materials
Building this DIY colorimeter requires a carefully selected set of components that balance performance, cost, and availability. The following table provides a complete parts list with specifications and estimated costs to help you source materials efficiently.
| Component | Specification | Quantity |
| ESP32 DevKit V1 | 30-pin or 38-pin | 1 |
| AS7265x Spectral Sensor | Triad version (all 3) | 1 |
| Breadboard | 830 tie-points | 1 |
| Jumper Wires | Male-to-Female | 10-15 |
| Cuvettes (Optical) | 1cm path length, plastic | 5-10 |
| USB Cable | Micro USB | 1 |
| 3D Printed Housing | Optional enclosure | 1 |
| Light Block Material | Black foam/cardboard | Small piece |
The total estimated cost ranges from approximately $80 to $115 depending on component sources and whether you opt for the 3D printed housing. The AS7265x spectral sensor represents the largest portion of the budget, but its capabilities justify the investment for serious colorimetry work. Consider purchasing from reputable electronics suppliers such as SparkFun, Digi-Key, or Mouser to ensure authentic components with proper factory calibration.
Circuit Diagram and Wiring Connections
The wiring for this project is remarkably straightforward thanks to the I2C interface employed by the AS7265x sensor. I2C (Inter-Integrated Circuit) communication requires only four connections between the ESP32 and the sensor: power, ground, serial data (SDA), and serial clock (SCL). This simplicity reduces the potential for wiring errors and makes the project accessible to builders with limited electronics experience.

Pin Connection Reference
| AS7265x Pin | ESP32 Pin | Function |
| VCC / 3.3V | 3.3V | Power supply (do not use 5V) |
| GND | GND | Common ground reference |
| SDA | GPIO 21 | I2C data line (default) |
| SCL | GPIO 22 | I2C clock line (default) |
Wiring Diagram Description
Begin by placing your ESP32 DevKit and AS7265x sensor on the breadboard. Connect the 3.3V pin from the ESP32 to the VCC pin on the sensor using a red jumper wire. It is critically important to use the 3.3V supply rather than the 5V pin, as the AS7265x is not 5V tolerant and will be damaged by higher voltages. Next, establish a common ground connection between the ESP32 GND pin and the sensor’s GND pin using a black jumper wire.
The I2C communication lines require two additional connections. Route a wire from ESP32 GPIO 21 (the default I2C SDA pin) to the sensor’s SDA pin, and another wire from ESP32 GPIO 22 (the default I2C SCL pin) to the sensor’s SCL pin. If you are using a breakout board version of the AS7265x, it may include built-in pull-up resistors on the I2C lines. If your breakout board does not include pull-up resistors, add 4.7kΩ resistors between each I2C line and 3.3V to ensure reliable communication.
Creating the Sample Chamber
Accurate colorimetric measurements require a light-tight sample chamber that positions the cuvette at a consistent distance from the sensor. For a professional-quality build, consider 3D printing a custom housing that holds the sensor and cuvette in precise alignment. Alternatively, you can construct a temporary chamber from black foam board or cardboard with the interior painted flat black to minimize stray light reflections.
Position the cuvette holder approximately 5-10mm from the sensor surface. The sensor’s built-in LED provides illumination for reflective measurements, but for transmission measurements (the standard colorimeter mode), you may need to add an external white LED on the opposite side of the cuvette. Ensure the light path passes through the sample and reaches the sensor without interference from ambient light by sealing any gaps in the chamber construction.
Build a Precision DIY Colorimeter with PCBWay—Custom PCBs & 3D-Printed Enclosures for Spectroscopy Projects
Transform your ESP32 and AS7265x spectroscopy sensor into a powerful, multi-channel colorimeter with PCBWay’s integrated manufacturing services. From high-performance custom PCBs to precision 3D-printed optical housings, we deliver everything you need to create a professional-grade spectral analysis tool.
Custom PCB Manufacturing for Multi-Spectral Precision:
PCBWay manufactures high-quality PCBs optimized for your AS7265x sensor array—capable of capturing 18 unique spectral channels across visible and near-infrared ranges. Our boards feature clean I²C signal routing, dedicated power planes for noise-sensitive analog sections, and proper impedance control for stable communication between your ESP32 and sensor. Choose from our professional assembly services, including SMT soldering of fine-pitch components, to receive fully populated, tested boards ready for firmware upload and immediate spectral measurements.
Light-Tight 3D-Printed Optical Enclosures:
Design and order custom enclosures through PCBWay’s advanced 3D printing service to ensure accurate, repeatable color measurements. Create light-sealed housings using matte-black ABS or nylon that block ambient interference, with precisely positioned sample cuvette holders and sensor windows. Incorporate mounting features that lock your PCB and sensor at optimal optical path lengths, ensuring consistent results across every measurement. Our high-resolution printing delivers the tight tolerances essential for optical applications.
Start Your Colorimeter Project Now with PCBWAY.com
Program Code and Software Setup
The software for this project consists of two main components: an Arduino sketch that runs on the ESP32 to handle sensor communication and WiFi connectivity, and a web interface that displays the spectral data in a user-friendly format. The complete code is provided below with detailed comments explaining each section’s function.
The project code is split into two files: DIYColorimeter.ino and webpage.h. The webpage.h file is separated to keep the code organized and to store all HTML, CSS, and JavaScript apart from the main program. To run the project, place both files in the Arduino sketch folder, use DIYColorimeter.ino as the main sketch, and include webpage.h with it.
YourSketchFolder/
├── Colorimeter.ino ← all C++ logic, sensor, WiFi, HTTP
└── webpage.h ← all HTML + CSS + JS, nothing else
Required Libraries
Before uploading the code, install the following libraries through the Arduino Library Manager:
- SparkFun_AS7265x – Official library for AS7265x sensor communication
- WiFi – Built-in ESP32 library for wireless connectivity
- WebServer – Built-in ESP32 library for HTTP server functionality
- ArduinoJson – For structured data handling (optional but recommended)
DIYcolorimeter.ino
/*
* ---------------
* DIY Colorimeter using AS7265x Triad Spectroscopy Sensor and ESP32 developed by Circuitschools.
*
* This file contains all C++ logic:
* - Sensor initialisation and reading
* - Spectral-to-RGB conversion (CIE 1931 pipeline)
* - WiFi connection
* - HTTP web server (dashboard + JSON data endpoint)
*
* The web dashboard HTML/CSS/JS lives in webpage.h and is kept
* completely separate to maintain clean project organisation.
*
* ---------------------------------------------------------------
* Circuit diagram and article is on Circuitschools.com
* Quick start
* 1. Set WIFI_SSID and WIFI_PASSWORD below.
* 2. Both DIYColorimeter.ino and webpage.h must be in the same sketch folder.
* 3. Upload. Open Serial Monitor at 115200 baud to get the device IP.
* 4. Open http://<device-ip> in any browser on the same network.
* ---------------------------------------------------------------
*/
#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#include "SparkFun_AS7265X.h"
#include "webpage.h" /* HTML_PAGE[] PROGMEM string */
/* ============================================================
WiFi credentials <-- EDIT THESE
============================================================ */
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASSWORD = "YOUR_PASSWORD";
/* ============================================================
Global objects
============================================================ */
AS7265X sensor;
WebServer server(80);
/* Latest computed colour values written by loop(), read by handleData() */
uint8_t gR = 0;
uint8_t gG = 0;
uint8_t gB = 0;
char gHex[8] = "#000000"; /* "#RRGGBB" + null terminator */
/* ============================================================
Spectral data container
Member names for the NIR die use N1-N6 (not R/S/T/U/V/W)
to avoid any ambiguity with local uint8_t r,g,b variables.
============================================================ */
struct SpectralData {
/* AS72651 - UV/Violet/Blue 410,435,460,485,510,535 nm */
float A, B, C, D, E, F;
/* AS72652 - Green to Red 560,585,610,645,680,705 nm */
float G, H, I, J, K, L;
/* AS72653 - NIR 730,760,810,860,900,940 nm */
float N1, N2, N3, N4, N5, N6;
};
/* ============================================================
CIE 1931 2-degree colour-matching functions
Sampled at the 12 visible AS7265x wavelengths (nm):
410,435,460,485,510,535,560,585,610,645,680,705
============================================================ */
static const float CIE_X[12] = {
0.014f, 0.073f, 0.134f, 0.045f, 0.009f, 0.063f,
0.290f, 0.743f, 1.065f, 0.866f, 0.283f, 0.068f
};
static const float CIE_Y[12] = {
0.000f, 0.002f, 0.004f, 0.012f, 0.040f, 0.130f,
0.329f, 0.700f, 0.862f, 0.631f, 0.175f, 0.042f
};
static const float CIE_Z[12] = {
0.068f, 0.356f, 0.691f, 0.253f, 0.048f, 0.018f,
0.011f, 0.006f, 0.003f, 0.001f, 0.000f, 0.000f
};
/* ============================================================
srgbGamma()
Plain static function - no lambdas, no auto, fully portable.
Applies the IEC 61966-2-1 piecewise sRGB transfer function.
============================================================ */
static float srgbGamma(float v)
{
if (v <= 0.0031308f) {
return 12.92f * v;
}
return 1.055f * powf(v, 1.0f / 2.4f) - 0.055f;
}
/* ============================================================
spectraToRGB()
Converts 12 visible spectral channels into 8-bit sRGB values.
Pipeline:
1. Clamp negative readings (sensor noise floor)
2. Normalise to peak channel = 1
3. Integrate CIE XYZ tristimulus values
4. XYZ -> linear sRGB (D65, IEC 61966-2-1 matrix)
5. Normalise brightest sRGB channel to 1 (hue-preserving)
6. Clamp to [0, 1]
7. Apply sRGB gamma, scale to 8-bit
============================================================ */
void spectraToRGB(const SpectralData& sd,
uint8_t& outR,
uint8_t& outG,
uint8_t& outB)
{
float ch[12] = {
sd.A, sd.B, sd.C, sd.D, sd.E, sd.F,
sd.G, sd.H, sd.I, sd.J, sd.K, sd.L
};
/* Step 1: clamp sensor noise */
int i;
for (i = 0; i < 12; i++) {
if (ch[i] < 0.0f) ch[i] = 0.0f;
}
/* Step 2: find peak for normalisation */
float peak = 0.0f;
for (i = 0; i < 12; i++) {
if (ch[i] > peak) peak = ch[i];
}
if (peak < 1.0e-6f) {
outR = outG = outB = 128;
return;
}
/* Step 3: integrate CIE XYZ */
float X = 0.0f, Y = 0.0f, Z = 0.0f;
for (i = 0; i < 12; i++) {
float n = ch[i] / peak;
X += n * CIE_X[i];
Y += n * CIE_Y[i];
Z += n * CIE_Z[i];
}
if ((X + Y + Z) < 1.0e-6f) {
outR = outG = outB = 0;
return;
}
/* Step 4: XYZ -> linear sRGB (D65 illuminant matrix) */
float Rl = 3.2406f * X - 1.5372f * Y - 0.4986f * Z;
float Gl = -0.9689f * X + 1.8758f * Y + 0.0415f * Z;
float Bl = 0.0557f * X - 0.2040f * Y + 1.0570f * Z;
/* Step 5: normalise so brightest channel = 1 */
float maxRGB = Rl;
if (Gl > maxRGB) maxRGB = Gl;
if (Bl > maxRGB) maxRGB = Bl;
if (maxRGB < 1.0e-6f) maxRGB = 1.0f;
Rl /= maxRGB;
Gl /= maxRGB;
Bl /= maxRGB;
/* Step 6: clamp */
if (Rl < 0.0f) Rl = 0.0f;
if (Rl > 1.0f) Rl = 1.0f;
if (Gl < 0.0f) Gl = 0.0f;
if (Gl > 1.0f) Gl = 1.0f;
if (Bl < 0.0f) Bl = 0.0f;
if (Bl > 1.0f) Bl = 1.0f;
/* Step 7: gamma encode and quantise */
outR = (uint8_t)(srgbGamma(Rl) * 255.0f + 0.5f);
outG = (uint8_t)(srgbGamma(Gl) * 255.0f + 0.5f);
outB = (uint8_t)(srgbGamma(Bl) * 255.0f + 0.5f);
}
/* ============================================================
readSensor()
Triggers a bulb measurement and copies all 18 calibrated
channel readings into a SpectralData struct.
============================================================ */
void readSensor(SpectralData& sd)
{
sensor.takeMeasurementsWithBulb();
sd.A = sensor.getCalibratedA();
sd.B = sensor.getCalibratedB();
sd.C = sensor.getCalibratedC();
sd.D = sensor.getCalibratedD();
sd.E = sensor.getCalibratedE();
sd.F = sensor.getCalibratedF();
sd.G = sensor.getCalibratedG();
sd.H = sensor.getCalibratedH();
sd.I = sensor.getCalibratedI();
sd.J = sensor.getCalibratedJ();
sd.K = sensor.getCalibratedK();
sd.L = sensor.getCalibratedL();
sd.N1 = sensor.getCalibratedR(); /* 730 nm */
sd.N2 = sensor.getCalibratedS(); /* 760 nm */
sd.N3 = sensor.getCalibratedT(); /* 810 nm */
sd.N4 = sensor.getCalibratedU(); /* 860 nm */
sd.N5 = sensor.getCalibratedV(); /* 900 nm */
sd.N6 = sensor.getCalibratedW(); /* 940 nm */
}
/* ============================================================
HTTP handler - GET /
Serves the full dashboard page from PROGMEM.
============================================================ */
void handleRoot()
{
server.send_P(200, "text/html", HTML_PAGE);
}
/* ============================================================
HTTP handler - GET /data
Takes a fresh measurement, recomputes RGB/HEX, and returns
a compact JSON object for the AJAX poller in the dashboard.
Response format:
{ "r":255, "g":128, "b":0, "hex":"#FF8000",
"channels":[c0,c1,...,c11] }
============================================================ */
void handleData()
{
SpectralData sd;
readSensor(sd);
uint8_t nr, ng, nb;
spectraToRGB(sd, nr, ng, nb);
/* Update globals so loop() serial print stays in sync */
gR = nr;
gG = ng;
gB = nb;
snprintf(gHex, sizeof(gHex), "#%02X%02X%02X",
(unsigned int)nr,
(unsigned int)ng,
(unsigned int)nb);
/* Build JSON without any external library */
String json = "{";
json += "\"r\":" + String((unsigned int)gR) + ",";
json += "\"g\":" + String((unsigned int)gG) + ",";
json += "\"b\":" + String((unsigned int)gB) + ",";
json += "\"hex\":\"" + String(gHex) + "\",";
json += "\"channels\":[";
float ch[12] = {
sd.A, sd.B, sd.C, sd.D, sd.E, sd.F,
sd.G, sd.H, sd.I, sd.J, sd.K, sd.L
};
int i;
for (i = 0; i < 12; i++) {
json += String(ch[i], 2);
if (i < 11) json += ",";
}
json += "]}";
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, "application/json", json);
}
/* ============================================================
setup()
============================================================ */
void setup()
{
Serial.begin(115200);
delay(400);
Serial.println("====== AS7265x DIY Colorimeter ======");
/* Initialise I2C bus (SDA=21, SCL=22 on most ESP32 boards) */
Wire.begin();
if (!sensor.begin()) {
Serial.println("[ERROR] AS7265x not found - check wiring and 3.3V supply!");
while (1) { delay(1000); }
}
Serial.println("[OK] AS7265x sensor ready.");
/* Sensor configuration */
sensor.setMeasurementMode(AS7265X_MEASUREMENT_MODE_6CHAN_CONTINUOUS);
sensor.setIntegrationCycles(49); /* 49 x ~2.8 ms = ~140 ms integration */
sensor.setGain(AS7265X_GAIN_16X); /* reduce to 1X or 4X if saturating */
sensor.disableIndicator(); /* turn off onboard status LED */
/* Connect to WiFi */
Serial.print("[WiFi] Connecting to ");
Serial.println(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int tries = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
tries++;
if (tries > 40) {
Serial.println("\n[ERROR] WiFi failed - check SSID/PASSWORD and retry.");
while (1) { delay(1000); }
}
}
Serial.println();
Serial.print("[WiFi] Connected! IP address: ");
Serial.println(WiFi.localIP());
Serial.print("[HTTP] Open http://");
Serial.print(WiFi.localIP());
Serial.println(" in your browser.");
/* Register URL routes and start server */
server.on("/", handleRoot);
server.on("/data", handleData);
server.begin();
Serial.println("[HTTP] Web server started.");
}
/* ============================================================
loop()
Handles incoming HTTP requests and refreshes the colour
globals once per second so they are always ready for /data.
============================================================ */
void loop()
{
server.handleClient();
static unsigned long lastMs = 0;
unsigned long now = millis();
if ((now - lastMs) >= 1000UL) {
lastMs = now;
SpectralData sd;
readSensor(sd);
uint8_t nr, ng, nb;
spectraToRGB(sd, nr, ng, nb);
gR = nr;
gG = ng;
gB = nb;
snprintf(gHex, sizeof(gHex), "#%02X%02X%02X",
(unsigned int)nr,
(unsigned int)ng,
(unsigned int)nb);
Serial.printf("[Colour] R:%3u G:%3u B:%3u HEX:%s\n",
(unsigned int)gR,
(unsigned int)gG,
(unsigned int)gB,
gHex);
}
}
webpage.h
/*
* All HTML, CSS, and JavaScript for the AS7265x Colorimeter dashboard.
* Stored in ESP32 flash (PROGMEM) as a plain const char array.
* Included by DIYColorimeter.ino - do NOT add any C++ logic here.
*
* Rules that keep this file safe across all ESP32 toolchain versions:
* - Pure ASCII only (no UTF-8 dashes, arrows, or special chars in C code)
* - No raw string literals (R"...") - uses a regular string with \n line joins
* - HTML entities (— • etc.) are fine inside the string content
* - Include guard prevents double-inclusion
*/
#ifndef WEBPAGE_H
#define WEBPAGE_H
#include <pgmspace.h>
/* Every line is a string literal; the compiler concatenates them.
No closing semicolons mid-way - just adjacent string chunks. */
const char HTML_PAGE[] PROGMEM =
"<!DOCTYPE html>\n"
"<html lang=\"en\">\n"
"<head>\n"
"<meta charset=\"UTF-8\"/>\n"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n"
"<title>AS7265x Colorimeter</title>\n"
"<link href=\"https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700"
"&family=DM+Sans:wght@300;400;600&display=swap\" rel=\"stylesheet\"/>\n"
"<style>\n"
":root{\n"
" --bg:#0a0a0f;\n"
" --panel:#13131a;\n"
" --border:#1e1e2e;\n"
" --accent:#e0e0ff;\n"
" --dim:#555577;\n"
" --mono:'Space Mono',monospace;\n"
" --sans:'DM Sans',sans-serif;\n"
"}\n"
"*{box-sizing:border-box;margin:0;padding:0}\n"
"body{\n"
" background:var(--bg);\n"
" color:var(--accent);\n"
" font-family:var(--sans);\n"
" min-height:100vh;\n"
" display:flex;\n"
" flex-direction:column;\n"
" align-items:center;\n"
" justify-content:center;\n"
"}\n"
"body::before{\n"
" content:'';\n"
" position:fixed;inset:0;\n"
" background-image:\n"
" linear-gradient(var(--border) 1px,transparent 1px),\n"
" linear-gradient(90deg,var(--border) 1px,transparent 1px);\n"
" background-size:40px 40px;\n"
" opacity:0.4;\n"
" pointer-events:none;\n"
" z-index:0;\n"
"}\n"
".container{\n"
" position:relative;z-index:1;\n"
" display:flex;flex-direction:column;\n"
" gap:24px;\n"
" width:min(520px,94vw);\n"
" padding:32px 0;\n"
"}\n"
"header{text-align:center}\n"
"header h1{\n"
" font-family:var(--mono);\n"
" font-size:clamp(1rem,4vw,1.4rem);\n"
" font-weight:700;letter-spacing:.18em;\n"
" text-transform:uppercase;color:#fff;\n"
"}\n"
"header p{\n"
" margin-top:6px;font-size:.75rem;\n"
" letter-spacing:.12em;color:var(--dim);\n"
" text-transform:uppercase;\n"
"}\n"
".swatch-card{\n"
" background:var(--panel);\n"
" border:1px solid var(--border);\n"
" border-radius:20px;overflow:hidden;\n"
" box-shadow:0 0 60px rgba(0,0,0,.6);\n"
"}\n"
".swatch{\n"
" height:210px;background:#1a1a2a;\n"
" transition:background .55s cubic-bezier(.4,0,.2,1);\n"
" position:relative;\n"
"}\n"
".swatch-label{\n"
" position:absolute;bottom:14px;right:18px;\n"
" font-family:var(--mono);font-size:.65rem;\n"
" letter-spacing:.12em;color:rgba(255,255,255,.3);\n"
" text-transform:uppercase;\n"
"}\n"
".metrics{\n"
" display:grid;grid-template-columns:repeat(4,1fr);\n"
" gap:1px;background:var(--border);\n"
"}\n"
".metric{\n"
" background:var(--panel);\n"
" padding:16px 8px 14px;text-align:center;\n"
"}\n"
".metric-lbl{\n"
" font-family:var(--mono);font-size:.58rem;\n"
" letter-spacing:.15em;color:var(--dim);\n"
" text-transform:uppercase;margin-bottom:8px;\n"
"}\n"
".metric-val{\n"
" font-family:var(--mono);font-size:1.1rem;\n"
" font-weight:700;color:#fff;\n"
"}\n"
"#valR{color:#ff6b6b}\n"
"#valG{color:#69db7c}\n"
"#valB{color:#74c0fc}\n"
"#valHex{font-size:.88rem;color:#e9d5ff}\n"
".spectrum-card{\n"
" background:var(--panel);\n"
" border:1px solid var(--border);\n"
" border-radius:16px;padding:20px 18px 16px;\n"
"}\n"
".spec-title{\n"
" font-family:var(--mono);font-size:.6rem;\n"
" letter-spacing:.18em;color:var(--dim);\n"
" text-transform:uppercase;margin-bottom:14px;\n"
"}\n"
".bars{\n"
" display:flex;align-items:flex-end;\n"
" gap:4px;height:68px;\n"
"}\n"
".bw{\n"
" flex:1;display:flex;flex-direction:column;\n"
" align-items:center;gap:3px;\n"
" height:100%;justify-content:flex-end;\n"
"}\n"
".bar{\n"
" width:100%;border-radius:3px 3px 0 0;\n"
" transition:height .5s cubic-bezier(.4,0,.2,1),opacity .5s;\n"
" min-height:2px;\n"
"}\n"
".bnm{\n"
" font-family:var(--mono);font-size:.44rem;\n"
" color:var(--dim);white-space:nowrap;\n"
"}\n"
".statusbar{\n"
" display:flex;align-items:center;\n"
" justify-content:space-between;\n"
" font-family:var(--mono);font-size:.6rem;\n"
" letter-spacing:.1em;color:var(--dim);\n"
" text-transform:uppercase;padding:0 4px;\n"
"}\n"
".dot{\n"
" width:6px;height:6px;border-radius:50%;\n"
" background:#69db7c;display:inline-block;\n"
" margin-right:8px;box-shadow:0 0 6px #69db7c;\n"
" animation:pulse 2s ease-in-out infinite;\n"
"}\n"
"@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}\n"
"</style>\n"
"</head>\n"
"<body>\n"
"<div class=\"container\">\n"
" <header>\n"
" <h1>AS7265x Colorimeter</h1>\n"
" <p>Spectral Analysis — ESP32 Live CircuitSchools.com</p>\n"
" </header>\n"
" <div class=\"swatch-card\">\n"
" <div class=\"swatch\" id=\"swatch\">\n"
" <span class=\"swatch-label\">Detected Colour</span>\n"
" </div>\n"
" <div class=\"metrics\">\n"
" <div class=\"metric\">\n"
" <div class=\"metric-lbl\">Red</div>\n"
" <div class=\"metric-val\" id=\"valR\">--</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-lbl\">Green</div>\n"
" <div class=\"metric-val\" id=\"valG\">--</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-lbl\">Blue</div>\n"
" <div class=\"metric-val\" id=\"valB\">--</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-lbl\">HEX</div>\n"
" <div class=\"metric-val\" id=\"valHex\">--</div>\n"
" </div>\n"
" </div>\n"
" </div>\n"
" <div class=\"spectrum-card\">\n"
" <div class=\"spec-title\">Spectral Power Distribution — Visible Channels</div>\n"
" <div class=\"bars\" id=\"bars\"></div>\n"
" </div>\n"
" <div class=\"statusbar\">\n"
" <span><span class=\"dot\"></span>Live • 1.2s refresh</span>\n"
" <span id=\"ts\">--</span>\n"
" </div>\n"
"</div>\n"
"<script>\n"
"var NM=[410,435,460,485,510,535,560,585,610,645,680,705];\n"
"var COLS=['#7b2fff','#6644ff','#2277ff','#00aaff','#00dd88','#44ee44',\n"
" '#aaee00','#ffdd00','#ffaa00','#ff6600','#ff2200','#dd0044'];\n"
"var barsEl=document.getElementById('bars');\n"
"var barEls=[];\n"
"for(var i=0;i<NM.length;i++){\n"
" var w=document.createElement('div');\n"
" w.className='bw';\n"
" var b=document.createElement('div');\n"
" b.className='bar';\n"
" b.style.background=COLS[i];\n"
" b.style.height='2px';\n"
" var lbl=document.createElement('div');\n"
" lbl.className='bnm';\n"
" lbl.textContent=NM[i];\n"
" w.appendChild(b);\n"
" w.appendChild(lbl);\n"
" barsEl.appendChild(w);\n"
" barEls.push(b);\n"
"}\n"
"function refresh(){\n"
" var xhr=new XMLHttpRequest();\n"
" xhr.open('GET','/data',true);\n"
" xhr.onreadystatechange=function(){\n"
" if(xhr.readyState!==4||xhr.status!==200)return;\n"
" var d;\n"
" try{d=JSON.parse(xhr.responseText);}catch(e){return;}\n"
" document.getElementById('swatch').style.background=d.hex;\n"
" document.getElementById('valR').textContent=d.r;\n"
" document.getElementById('valG').textContent=d.g;\n"
" document.getElementById('valB').textContent=d.b;\n"
" document.getElementById('valHex').textContent=d.hex;\n"
" document.getElementById('ts').textContent=new Date().toLocaleTimeString();\n"
" var ch=d.channels;\n"
" var mx=1;\n"
" for(var j=0;j<ch.length;j++){if(ch[j]>mx)mx=ch[j];}\n"
" for(var j=0;j<ch.length;j++){\n"
" var frac=ch[j]/mx;\n"
" var h=frac*64;\n"
" barEls[j].style.height=(h<2?2:h)+'px';\n"
" barEls[j].style.opacity=0.45+0.55*frac;\n"
" }\n"
" };\n"
" xhr.send();\n"
"}\n"
"refresh();\n"
"setInterval(refresh,1200);\n"
"</script>\n"
"</body>\n"
"</html>\n"
;
#endif /* WEBPAGE_H */
Code Overview
Two files, one job: measure light spectrum → compute colour → serve a live web dashboard.
webpage.h — The Frontend
A single PROGMEM string (stored in ESP32 flash, not RAM) containing a complete HTML page with three sections:
- Colour swatch — a large box that animates to the detected colour
- RGB + HEX metrics — four tiles showing R, G, B values and the hex code
- Spectral bar chart — 12 bars (one per visible wavelength) showing relative intensity
The JavaScript uses a plain XMLHttpRequest to poll /data every 1.2 seconds and updates the UI without any page refresh.
DIYColorimeter.ino — The Backend
5 logical stages:
1. Read sensor — readSensor() fires the AS7265x’s white bulb, waits for all 3 dies to finish, then reads 18 calibrated floating-point channel values (6 per die covering 410–940 nm).
2. Spectral → RGB — spectraToRGB() runs the physics pipeline:
- Clamps noise, normalises to peak channel
- Integrates CIE XYZ tristimulus values using tabulated colour-matching functions
- Converts XYZ → linear sRGB via the D65 illuminant matrix
- Normalises brightness, clamps, then applies sRGB gamma to get final 8-bit R, G, B
3. Format HEX — snprintf converts the three bytes into #RRGGBB
4. Web server — Two routes:
GET /→ sends the full HTML page onceGET /data→ triggers a fresh measurement and returns a compact JSON{r, g, b, hex, channels[12]}
5. Loop — Calls server.handleClient() to process requests, and independently refreshes the colour globals every second so serial output stays live even when no browser is connected.
Output on Serial monitor and Webserver
After uploading the code open serial monitor with baudrate 115200, you can see the webserver IP address, which you can enter on any device connected to the same network to display the dashboard.

and when we open the IP address from the Serial monitor we can see the dashboard as below

Practical Applications
This DIY colorimeter offers many possibilities for scientific exploration and measurement. Here are some of the most valuable applications that you can pursue with this project:
Water Quality Analysis: Measure turbidity, chlorine content, and detect various water pollutants based on their unique absorption patterns.
pH Indication: Employ pH indicators that change color upon reacting with acids and bases. The color change can be measured with high precision.
Nutrient Solution Testing: Hydroponic gardeners can now monitor nutrient levels by detecting specific patterns that represent nitrogen, phosphorus, and potassium.
Food and Beverage Analysis: Measure color intensity in various food products such as wine, beer, fruit juices, and cooking oils.
Educational Demonstration: The web interface makes it easy to demonstrate principles of spectroscopy, light absorption, and color science.
Material Identification: Various materials display unique patterns. Create a library of known materials and use it as a reference to identify unknown materials.
Troubleshooting Common Issues
Even well-constructed projects may encounter problems. Here are solutions to the most frequently reported issues:
Sensor Not Detected
If the serial monitor displays “Sensor not found,” verify all I2C connections are secure and correctly routed. The sensor requires both power and the two communication lines. Check that you are using 3.3V rather than 5V for power. An I2C scanner sketch can help identify whether any I2C devices are visible on the bus. If the scanner finds nothing, inspect the wiring for breaks or incorrect connections.
Inconsistent or Erratic Readings
Inconsistent measurements typically result from ambient light contamination or unstable sample positioning. Ensure your sample chamber is completely light-tight by testing with the LED disabled—the sensor should read near zero in complete darkness. Check that cuvettes are consistently oriented and positioned at the same distance from the sensor for each measurement. Increasing integration time can improve signal-to-noise ratio for more stable readings.
WiFi Connection Failures
Connection problems often stem from incorrect credentials or weak WiFi signals. Double-check your SSID and password, paying attention to capitalization and special characters. The ESP32’s WiFi radio is relatively sensitive to interference, so try positioning the device closer to your router during initial testing. Some corporate and public networks may block device-to-device communication, preventing access to the web server.
Web Interface Not Loading
If the web interface fails to load, confirm that your computer or phone is connected to the same WiFi network as the ESP32. The IP address displayed in the serial monitor must be entered correctly in your browser. Some browsers may cache old page versions; try a hard refresh (Ctrl+F5) or clear browser cache if changes to the code don’t appear to take effect.
Conclusion
With the DIY colorimeter built using the AS7265x Spectroscopy Sensor and ESP32 Web Server, you will have an analytical tool that rivals those that cost many times more. With 18 channel spectral capability, you will be able to gather detailed color information that is not possible with simple RGB sensors. Plus, the web interface will allow you to access your data from any device on your network without having to install any software.
Not only will you have an excellent tool, but you will also have learned skills that will be essential when you want to create even more complex projects. By using the skills you have learned in this project, you will be able to create even more complex projects using data logging to an SD card, using cloud services to access your data remotely, or using automated measurement sequences for quality control.
Whether you are a student trying to learn about spectroscopy, a hobbyist interested in learning about color, or a professional trying to provide your company with an affordable tool, you will be able to use this project to get started in the fascinating world of spectroscopy.
About This Project
This DIY colorimeter project demonstrates practical applications of spectroscopy sensors with ESP32 microcontrollers. Related search terms include: AS7265x tutorial, ESP32 spectroscopy, DIY spectrophotometer, Arduino color sensor, WiFi colorimeter, spectral analysis Arduino, homemade spectrometer, and color measurement device. The project is suitable for intermediate electronics hobbyists with basic soldering and programming experience.
