Add Web Serial interface for CIE color visualization

- Add GitHub Pages web interface in /docs directory
- Add WebSerial example Arduino sketch
- Implement color plotting on CIE 1931 chromaticity diagram
- Add realtime color visualization and conversion

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ladyada 2025-05-14 10:04:59 -04:00
parent c2239f97f6
commit b7a837265e
6 changed files with 572 additions and 0 deletions

3
CIE1931xy_CIERGB.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

48
docs/README.md Normal file
View file

@ -0,0 +1,48 @@
# OPT4048 CIE Color Plotter
This web interface allows you to visualize color measurements from an Adafruit OPT4048 color sensor in real-time using the Web Serial API.
## How to Use
1. **Upload the Arduino sketch**: First, upload the `opt4048_webserial.ino` sketch from the examples folder to your Arduino board.
2. **Connect to this web page**: You can either:
- Host the page locally
- Use GitHub Pages (if this repository is published there)
3. **Connect to your Arduino**: Click the "Connect to Arduino" button and select your Arduino from the popup menu.
4. **View measurements**: The sensor readings will appear on the CIE chromaticity diagram, showing you exactly where the measured color falls in the CIE color space.
## Features
- Displays CIE x,y coordinates in real-time
- Plots the color point on a standard CIE 1931 chromaticity diagram
- Shows lux (brightness) and color temperature (CCT) values
- Provides approximate RGB color visualization
- Monitors serial output for debugging
## Browser Compatibility
This interface uses the Web Serial API, which is currently supported in:
- Google Chrome (version 89+)
- Microsoft Edge (version 89+)
- Opera (version 75+)
It is **not** supported in Firefox or Safari due to their Web Serial API implementation status.
## About the OPT4048 Sensor
The OPT4048 is a high-precision tristimulus XYZ color sensor by Texas Instruments. The Adafruit breakout board makes it easy to interface with this sensor using I2C.
This sensor provides accurate color measurements in XYZ color space, which can be converted to standard CIE 1931 xy chromaticity coordinates.
## File Structure
- `index.html` - The main webpage
- `script.js` - JavaScript code for communication and visualization
- `cie1931_diagram.svg` - SVG image of the CIE 1931 chromaticity diagram
## License
MIT license, all text here must be included in any redistribution

3
docs/cie1931_diagram.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

156
docs/index.html Normal file
View file

@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OPT4048 CIE Color Plotter</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.controls {
flex: 1;
min-width: 300px;
}
.visualization {
flex: 2;
min-width: 500px;
}
#cie-diagram {
position: relative;
width: 100%;
max-width: 600px;
margin-bottom: 20px;
}
#cie-diagram img {
width: 100%;
height: auto;
display: block;
}
#data-point {
position: absolute;
width: 10px;
height: 10px;
background-color: red;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
z-index: 10;
}
#color-sample {
width: 30px;
height: 30px;
border: 1px solid #ccc;
margin: 0 auto;
border-radius: 50%;
}
#serial-log {
height: 200px;
overflow-y: auto;
background-color: #f5f5f5;
padding: 10px;
border: 1px solid #ddd;
font-family: monospace;
}
button {
padding: 10px 16px;
margin: 5px 0;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.data-display {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.data-box {
flex: 1;
margin: 0 10px;
padding: 15px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
}
.data-box h3 {
margin-top: 0;
}
.data-value {
font-size: 24px;
font-weight: bold;
}
</style>
</head>
<body>
<h1>OPT4048 CIE Color Plotter</h1>
<p>Connect your Arduino with OPT4048 sensor to visualize color measurements on a CIE diagram.</p>
<div class="container">
<div class="controls">
<button id="connect-button">Connect to Arduino</button>
<button id="disconnect-button" disabled>Disconnect</button>
<button id="clear-button">Clear Log</button>
<h2>Connection Status</h2>
<p id="status">Not connected</p>
<h2>Serial Monitor</h2>
<div id="serial-log"></div>
</div>
<div class="visualization">
<h2>CIE 1931 Chromaticity Diagram</h2>
<div id="cie-diagram">
<img src="cie1931_diagram.svg" alt="CIE 1931 Chromaticity Diagram">
<div id="data-point" style="display: none;"></div>
</div>
<div class="data-display">
<div class="data-box">
<h3>CIE x</h3>
<div id="cie-x" class="data-value">-</div>
</div>
<div class="data-box">
<h3>CIE y</h3>
<div id="cie-y" class="data-value">-</div>
</div>
<div class="data-box">
<h3>Lux</h3>
<div id="lux" class="data-value">-</div>
</div>
<div class="data-box">
<h3>CCT (K)</h3>
<div id="cct" class="data-value">-</div>
</div>
</div>
<div class="data-box">
<h3>Color Approximation</h3>
<div id="color-sample"></div>
<small>(Note: This is a rough approximation)</small>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

283
docs/script.js Normal file
View file

@ -0,0 +1,283 @@
// Global variables
let port;
let reader;
let writer;
let readTimeout;
let keepReading = false;
let decoder = new TextDecoder();
let lineBuffer = '';
// DOM Elements
const connectButton = document.getElementById('connect-button');
const disconnectButton = document.getElementById('disconnect-button');
const clearButton = document.getElementById('clear-button');
const statusDisplay = document.getElementById('status');
const serialLog = document.getElementById('serial-log');
const dataPoint = document.getElementById('data-point');
const cieXDisplay = document.getElementById('cie-x');
const cieYDisplay = document.getElementById('cie-y');
const luxDisplay = document.getElementById('lux');
const cctDisplay = document.getElementById('cct');
const colorSample = document.getElementById('color-sample');
// Check if Web Serial API is supported
if ('serial' in navigator) {
connectButton.addEventListener('click', connectToArduino);
disconnectButton.addEventListener('click', disconnectFromArduino);
clearButton.addEventListener('click', clearLog);
} else {
statusDisplay.textContent = 'Web Serial API not supported in this browser. Try Chrome or Edge.';
connectButton.disabled = true;
}
// Connect to Arduino via Web Serial
async function connectToArduino() {
try {
// Request a port and open a connection
port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
// Set up the reader and writer
reader = port.readable.getReader();
writer = port.writable.getWriter();
// Enable/disable buttons
connectButton.disabled = true;
disconnectButton.disabled = false;
statusDisplay.textContent = 'Connected to Arduino';
addToLog('Connected to Arduino', 'status');
// Start reading data
keepReading = true;
readSerialData();
} catch (error) {
console.error('Error connecting to Arduino:', error);
addToLog(`Error connecting: ${error.message}`, 'error');
statusDisplay.textContent = 'Connection failed';
}
}
// Disconnect from Arduino
async function disconnectFromArduino() {
if (reader) {
keepReading = false;
clearTimeout(readTimeout);
try {
await reader.cancel();
await reader.releaseLock();
reader = null;
} catch (error) {
console.error('Error releasing reader:', error);
}
}
if (writer) {
try {
await writer.close();
writer = null;
} catch (error) {
console.error('Error releasing writer:', error);
}
}
if (port) {
try {
await port.close();
port = null;
} catch (error) {
console.error('Error closing port:', error);
}
}
// Update UI
connectButton.disabled = false;
disconnectButton.disabled = true;
statusDisplay.textContent = 'Disconnected';
addToLog('Disconnected from Arduino', 'status');
hideDataPoint();
}
// Read data from the serial port
async function readSerialData() {
while (port && keepReading) {
try {
const { value, done } = await reader.read();
if (done) {
// Reader has been canceled
break;
}
// Process the received data
processSerialData(decoder.decode(value));
} catch (error) {
console.error('Error reading data:', error);
addToLog(`Error reading data: ${error.message}`, 'error');
break;
}
}
// If we exited the loop without being explicitly disconnected
if (keepReading) {
disconnectFromArduino();
}
}
// Process data received from Arduino
function processSerialData(data) {
// Add received data to the buffer
lineBuffer += data;
// Process complete lines
let lineEnd;
while ((lineEnd = lineBuffer.indexOf('\n')) !== -1) {
const line = lineBuffer.substring(0, lineEnd).trim();
lineBuffer = lineBuffer.substring(lineEnd + 1);
if (line) {
addToLog(line);
parseDataFromLine(line);
}
}
}
// Parse data from a line received from Arduino
function parseDataFromLine(line) {
// Look for CIE x value
const cieXMatch = line.match(/CIE x: ([\d.]+)/);
if (cieXMatch) {
const cieX = parseFloat(cieXMatch[1]);
cieXDisplay.textContent = cieX.toFixed(6);
}
// Look for CIE y value
const cieYMatch = line.match(/CIE y: ([\d.]+)/);
if (cieYMatch) {
const cieY = parseFloat(cieYMatch[1]);
cieYDisplay.textContent = cieY.toFixed(6);
// If we have both x and y, update the plot
if (cieXMatch) {
const cieX = parseFloat(cieXMatch[1]);
updateCIEPlot(cieX, cieY);
}
}
// Look for Lux value
const luxMatch = line.match(/Lux: ([\d.]+)/);
if (luxMatch) {
const lux = parseFloat(luxMatch[1]);
luxDisplay.textContent = lux.toFixed(2);
}
// Look for Color Temperature value
const cctMatch = line.match(/Color Temperature: ([\d.]+)/);
if (cctMatch) {
const cct = parseFloat(cctMatch[1]);
cctDisplay.textContent = cct.toFixed(0);
}
}
// Update the CIE plot with new data point
function updateCIEPlot(x, y) {
// Get the dimensions of the CIE diagram image
const cieImage = document.querySelector('#cie-diagram img');
const imageRect = cieImage.getBoundingClientRect();
// The SVG has a coordinate system where:
// - X axis is marked from 0.0 to 0.8 (matched to pixel positions 60 to 470)
// - Y axis is marked from 0.0 to 0.9 (matched to pixel positions 476 to 15)
// Define the SVG's coordinate mapping
const svgXMin = 60, svgXMax = 470; // Left and right edge pixel positions in SVG
const svgYMin = 476, svgYMax = 15; // Bottom and top edge pixel positions in SVG
const cieXMin = 0.0, cieXMax = 0.8; // Min and max x chromaticity values
const cieYMin = 0.0, cieYMax = 0.9; // Min and max y chromaticity values
// Map the CIE coordinates to percentages within the SVG viewport
const svgWidth = svgXMax - svgXMin;
const svgHeight = svgYMin - svgYMax;
// Calculate the percentage position (normalize to SVG viewBox)
const xPercent = ((x - cieXMin) / (cieXMax - cieXMin)) * 100;
const yPercent = (1 - ((y - cieYMin) / (cieYMax - cieYMin))) * 100; // Invert y-axis
// Set the data point position
dataPoint.style.left = `${xPercent}%`;
dataPoint.style.top = `${yPercent}%`;
dataPoint.style.display = 'block';
// Update the color sample with an approximate RGB color
updateColorSample(x, y);
}
// Convert CIE XYZ to RGB for color approximation
function updateColorSample(x, y) {
// Calculate XYZ from xyY (assuming Y=1 for relative luminance)
const Y = 1.0;
const X = (x * Y) / y;
const Z = ((1 - x - y) * Y) / y;
// XYZ to RGB conversion (sRGB)
// Using the standard D65 transformation matrix
let r = X * 3.2406 - Y * 1.5372 - Z * 0.4986;
let g = -X * 0.9689 + Y * 1.8758 + Z * 0.0415;
let b = X * 0.0557 - Y * 0.2040 + Z * 1.0570;
// Apply gamma correction
r = r <= 0.0031308 ? 12.92 * r : 1.055 * Math.pow(r, 1/2.4) - 0.055;
g = g <= 0.0031308 ? 12.92 * g : 1.055 * Math.pow(g, 1/2.4) - 0.055;
b = b <= 0.0031308 ? 12.92 * b : 1.055 * Math.pow(b, 1/2.4) - 0.055;
// Clamp RGB values between 0 and 1
r = Math.min(Math.max(0, r), 1);
g = Math.min(Math.max(0, g), 1);
b = Math.min(Math.max(0, b), 1);
// Convert to 8-bit color values
const ri = Math.round(r * 255);
const gi = Math.round(g * 255);
const bi = Math.round(b * 255);
// Set the background color of the sample
colorSample.style.backgroundColor = `rgb(${ri}, ${gi}, ${bi})`;
}
// Hide the data point and reset all displays
function hideDataPoint() {
dataPoint.style.display = 'none';
cieXDisplay.textContent = '-';
cieYDisplay.textContent = '-';
luxDisplay.textContent = '-';
cctDisplay.textContent = '-';
colorSample.style.backgroundColor = 'transparent';
}
// Add a message to the serial log
function addToLog(message, type = 'data') {
const entry = document.createElement('div');
entry.textContent = message;
entry.className = `log-entry ${type}`;
serialLog.appendChild(entry);
serialLog.scrollTop = serialLog.scrollHeight;
}
// Clear the serial log
function clearLog() {
serialLog.innerHTML = '';
}
// Send a command to the Arduino
async function sendCommand(command) {
if (writer) {
try {
const encoder = new TextEncoder();
await writer.write(encoder.encode(command + '\n'));
addToLog(`Sent: ${command}`, 'command');
} catch (error) {
console.error('Error sending command:', error);
addToLog(`Error sending command: ${error.message}`, 'error');
}
}
}

View file

@ -0,0 +1,79 @@
/*!
* @file opt4048_webserial.ino
*
* This example reads color data from the OPT4048 sensor and outputs it
* in a format suitable for displaying on a web page using Web Serial API.
*
* It continuously measures CIE x,y coordinates, lux, and color temperature.
*/
#include <Wire.h>
#include "Adafruit_OPT4048.h"
// Create sensor object
Adafruit_OPT4048 sensor;
// Set how often to read data (in milliseconds)
const unsigned long READ_INTERVAL = 100;
unsigned long lastReadTime = 0;
void setup() {
// Initialize serial communication at 115200 baud
Serial.begin(115200);
// Wait briefly for serial to connect (not needed for all boards)
delay(100);
Serial.println(F("Adafruit OPT4048 WebSerial Example"));
Serial.println(F("This sketch works with the OPT4048 CIE Color Plotter web page"));
// Initialize the sensor
if (!sensor.begin()) {
Serial.println(F("Failed to find OPT4048 chip"));
while (1) {
delay(10);
}
}
Serial.println(F("OPT4048 sensor found!"));
// Set sensor configuration
sensor.setRange(OPT4048_RANGE_AUTO); // Auto-range for best results across lighting conditions
sensor.setConversionTime(OPT4048_CONVERSION_TIME_100MS); // 100ms conversion time
sensor.setMode(OPT4048_MODE_CONTINUOUS); // Continuous mode
}
void loop() {
// Only read at the specified interval
unsigned long currentTime = millis();
if (currentTime - lastReadTime >= READ_INTERVAL) {
lastReadTime = currentTime;
// Calculate and display CIE chromaticity coordinates and lux
double CIEx, CIEy, lux;
if (sensor.getCIE(&CIEx, &CIEy, &lux)) {
// Print the values in a format that can be easily parsed by the web page
Serial.println(F("---CIE Data---"));
Serial.print(F("CIE x: ")); Serial.println(CIEx, 8);
Serial.print(F("CIE y: ")); Serial.println(CIEy, 8);
Serial.print(F("Lux: ")); Serial.println(lux, 4);
// Calculate and display color temperature
double colorTemp = sensor.calculateColorTemperature(CIEx, CIEy);
Serial.print(F("Color Temperature: "));
Serial.print(colorTemp, 2);
Serial.println(F(" K"));
Serial.println(F("-------------"));
} else {
Serial.println(F("Error reading sensor data"));
}
}
// Check for any incoming serial commands
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
command.trim();
// Process any commands here if needed
}
}