From b7a837265eee14f49e84754c9ebc8cd042dec99b Mon Sep 17 00:00:00 2001 From: ladyada Date: Wed, 14 May 2025 10:04:59 -0400 Subject: [PATCH] Add Web Serial interface for CIE color visualization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CIE1931xy_CIERGB.svg | 3 + docs/README.md | 48 +++ docs/cie1931_diagram.svg | 3 + docs/index.html | 156 ++++++++++ docs/script.js | 283 ++++++++++++++++++ .../opt4048_webserial/opt4048_webserial.ino | 79 +++++ 6 files changed, 572 insertions(+) create mode 100644 CIE1931xy_CIERGB.svg create mode 100644 docs/README.md create mode 100644 docs/cie1931_diagram.svg create mode 100644 docs/index.html create mode 100644 docs/script.js create mode 100644 examples/opt4048_webserial/opt4048_webserial.ino diff --git a/CIE1931xy_CIERGB.svg b/CIE1931xy_CIERGB.svg new file mode 100644 index 0000000..f05f9d5 --- /dev/null +++ b/CIE1931xy_CIERGB.svg @@ -0,0 +1,3 @@ + + + 460480500520540560580600620x0.00.10.20.30.40.50.60.70.8y0.00.10.20.30.40.50.60.70.80.9E diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..70efe76 --- /dev/null +++ b/docs/README.md @@ -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 \ No newline at end of file diff --git a/docs/cie1931_diagram.svg b/docs/cie1931_diagram.svg new file mode 100644 index 0000000..f05f9d5 --- /dev/null +++ b/docs/cie1931_diagram.svg @@ -0,0 +1,3 @@ + + + 460480500520540560580600620x0.00.10.20.30.40.50.60.70.8y0.00.10.20.30.40.50.60.70.80.9E diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..ebeb066 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,156 @@ + + + + + + OPT4048 CIE Color Plotter + + + +

OPT4048 CIE Color Plotter

+

Connect your Arduino with OPT4048 sensor to visualize color measurements on a CIE diagram.

+ +
+
+ + + + +

Connection Status

+

Not connected

+ +

Serial Monitor

+
+
+ +
+

CIE 1931 Chromaticity Diagram

+
+ CIE 1931 Chromaticity Diagram + +
+ +
+
+

CIE x

+
-
+
+
+

CIE y

+
-
+
+
+

Lux

+
-
+
+
+

CCT (K)

+
-
+
+
+ +
+

Color Approximation

+
+ (Note: This is a rough approximation) +
+
+
+ + + + \ No newline at end of file diff --git a/docs/script.js b/docs/script.js new file mode 100644 index 0000000..9b81e8a --- /dev/null +++ b/docs/script.js @@ -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'); + } + } +} \ No newline at end of file diff --git a/examples/opt4048_webserial/opt4048_webserial.ino b/examples/opt4048_webserial/opt4048_webserial.ino new file mode 100644 index 0000000..9c938fd --- /dev/null +++ b/examples/opt4048_webserial/opt4048_webserial.ino @@ -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 +#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 + } +} \ No newline at end of file