diff --git a/LICENSES/LicenseRef-Wikipedia-Public-Domain.txt b/LICENSES/LicenseRef-Wikipedia-Public-Domain.txt new file mode 100644 index 0000000..19087f6 --- /dev/null +++ b/LICENSES/LicenseRef-Wikipedia-Public-Domain.txt @@ -0,0 +1,3 @@ +I, the copyright holder of this work, release this work into the public domain. This applies worldwide. +In some countries this may not be legally possible; if so: +I grant anyone the right to use this work for any purpose, without any conditions, unless such conditions are required by law. diff --git a/adafruit_opt4048.py b/adafruit_opt4048.py index 7a154d0..ef9866c 100644 --- a/adafruit_opt4048.py +++ b/adafruit_opt4048.py @@ -323,7 +323,6 @@ def __init__(self, i2c_bus, address=_OPT4048_DEFAULT_ADDR): def init(self): """Initialize the sensor and verify the device ID""" # Check device ID - print(f"id: {hex(self._device_id)}") if self._device_id != _OPT4048_CHIP_ID: raise RuntimeError("Failed to find an OPT4048 sensor - check your wiring!") @@ -593,7 +592,6 @@ def threshold_low(self): # Read the exponent and mantissa from the threshold low register exponent = self._threshold_low_exponent mantissa = self._threshold_low_mantissa - print(f"exponent: {exponent} mantissa: {mantissa}") # Calculate ADC code value by applying the exponent as a bit shift # ADD 8 to the exponent as per datasheet equations 12-13 return mantissa << (8 + exponent) @@ -697,7 +695,6 @@ def all_channels(self): # Combine MSB and LSB to form the 20-bit mantissa mant = (msb << 8) | lsb - # print(f"ch: {ch} exp: {exp} msb: {msb} lsb: {lsb} counter: {counter} crc: {crc} mant: {mant}") # noqa: E501 # Calculate CRC # Initialize CRC variables x0 = 0 # CRC bit 0 diff --git a/examples/opt4048_websocket.py b/examples/opt4048_websocket.py new file mode 100644 index 0000000..8d6a0e7 --- /dev/null +++ b/examples/opt4048_websocket.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 Tim C for Adafruit Industries +# SPDX-License-Identifier: MIT + +from asyncio import create_task, gather, run +from asyncio import sleep as async_sleep + +import board +import socketpool +import wifi +from adafruit_httpserver import GET, FileResponse, Request, Response, Server, Websocket + +from adafruit_opt4048 import OPT4048, ConversionTime, Mode, Range + +pool = socketpool.SocketPool(wifi.radio) +server = Server(pool, debug=True, root_path="opt4048_ws_static") + +websocket: Websocket = None + +READ_INTERVAL = 0.1 # seconds + +i2c = board.I2C() # uses board.SCL and board.SDA +# i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller +sensor = OPT4048(i2c) + +sensor.range = Range.AUTO +sensor.conversion_time = ConversionTime.TIME_100MS +sensor.mode = Mode.CONTINUOUS + + +@server.route("/connect-websocket", GET) +def connect_client(request: Request): + global websocket # noqa: PLW0603, global use + + if websocket is not None: + websocket.close() # Close any existing connection + + websocket = Websocket(request) + + return websocket + + +server.start(str(wifi.radio.ipv4_address)) + + +async def handle_http_requests(): + while True: + server.poll() + + await async_sleep(0) + + +async def send_color_data_ws(): + while True: + if websocket is not None: + try: + x, y, lux = sensor.cie + out_msg = "---CIE Data---\n" + out_msg += f"CIE x: {x}\n" + out_msg += f"CIE y: {y}\n" + out_msg += f"Lux: {lux}\n" + out_msg += f"Color Temperature: {sensor.calculate_color_temperature(x, y)} K\n" + out_msg += "-------------\n" + + websocket.send_message(out_msg, fail_silently=True) + except RuntimeError: + # error reading sensor + pass + + await async_sleep(READ_INTERVAL) + + +async def main(): + await gather( + create_task(handle_http_requests()), + create_task(send_color_data_ws()), + ) + + +run(main()) diff --git a/examples/opt4048_ws_static/cie1931_diagram.svg b/examples/opt4048_ws_static/cie1931_diagram.svg new file mode 100644 index 0000000..95d095f --- /dev/null +++ b/examples/opt4048_ws_static/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.9 diff --git a/examples/opt4048_ws_static/cie1931_diagram.svg.license b/examples/opt4048_ws_static/cie1931_diagram.svg.license new file mode 100644 index 0000000..a5f35b1 --- /dev/null +++ b/examples/opt4048_ws_static/cie1931_diagram.svg.license @@ -0,0 +1,2 @@ +// SPDX-FileCopyrightText: Copyright (c) 2009 BenRG +// SPDX-License-Identifier: LicenseRef-Wikipedia-Public-Domain diff --git a/examples/opt4048_ws_static/index.html b/examples/opt4048_ws_static/index.html new file mode 100644 index 0000000..c9e6a14 --- /dev/null +++ b/examples/opt4048_ws_static/index.html @@ -0,0 +1,202 @@ + + + + + + + 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) +
+
+
+ + + + diff --git a/examples/opt4048_ws_static/script.js b/examples/opt4048_ws_static/script.js new file mode 100644 index 0000000..b2097a7 --- /dev/null +++ b/examples/opt4048_ws_static/script.js @@ -0,0 +1,271 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 Tim C for Adafruit Industries +// SPDX-License-Identifier: MIT + +// Global variables +let port; +let reader; +let writer; +let readTimeout; +let keepReading = false; +let decoder = new TextDecoder(); +let lineBuffer = ''; + +// DOM Elements + +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'); +const debugCoordinates = document.getElementById('debug-coordinates'); + +// Add a test plotting function +function testPlotPoint() { + // Test with fixed values at 25%, 50%, and 75% across the CIE diagram + const testPoints = [ + { x: 0.2, y: 0.3, label: "Test point 1 (0.2, 0.3)" }, + { x: 0.4, y: 0.45, label: "Test point 2 (0.4, 0.45)" }, + { x: 0.6, y: 0.6, label: "Test point 3 (0.6, 0.6)" } + ]; + + // Get next test point (rotate through them) + const currentTest = parseInt(localStorage.getItem('currentTestPoint') || '0'); + const nextTest = (currentTest + 1) % testPoints.length; + localStorage.setItem('currentTestPoint', nextTest); + + const testPoint = testPoints[nextTest]; + + // Update the data displays + cieXDisplay.textContent = testPoint.x.toFixed(6); + cieYDisplay.textContent = testPoint.y.toFixed(6); + + // Call the plot function + updateCIEPlot(testPoint.x, testPoint.y); + + // Show test information + addToLog(`Testing point: ${testPoint.label}`, 'status'); + debugCoordinates.textContent = `TEST MODE: ${testPoint.label}`; +} + +clearButton.addEventListener('click', clearLog); + +let ws = new WebSocket('ws://' + location.host + '/connect-websocket'); + +ws.onopen = () => { + console.log('WebSocket connection opened'); + statusDisplay.innerText = "Connected"; +} + +ws.onclose = () => { + console.log('WebSocket connection closed'); + statusDisplay.innerText = "Not Connected"; + hideDataPoint(); +} + +ws.onmessage = ws_onmessage; +ws.onerror = error => console.log(error); + + +function ws_onmessage(event){ + processSerialData(event.data) +} + + +// Process data received from MCU +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 MCU +function parseDataFromLine(line) { + // Log the raw line + console.log("Data received:", 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); + console.log("Found CIE x:", cieX); + + // If we have a y value already stored in the display + const cieYStr = cieYDisplay.textContent; + if (cieYStr !== '-') { + const cieY = parseFloat(cieYStr); + console.log("Using existing CIE y:", cieY); + if (!isNaN(cieY)) { + // Update the plot with the current x,y pair + updateCIEPlot(cieX, cieY); + } + } + } + + // Look for CIE y value + const cieYMatch = line.match(/CIE y: ([\d.]+)/); + if (cieYMatch) { + const cieY = parseFloat(cieYMatch[1]); + cieYDisplay.textContent = cieY.toFixed(6); + console.log("Found CIE y:", cieY); + + // If we have an x value already stored in the display + const cieXStr = cieXDisplay.textContent; + if (cieXStr !== '-' && cieXMatch === null) { // Only use stored x if not found on this line + const cieX = parseFloat(cieXStr); + console.log("Using existing CIE x:", cieX); + if (!isNaN(cieX)) { + // Update the plot with the current x,y pair + 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); + console.log("Found Lux:", lux); + } + + // Look for Color Temperature value + const cctMatch = line.match(/Color Temperature: ([\d.]+)/); + if (cctMatch) { + const cct = parseFloat(cctMatch[1]); + cctDisplay.textContent = cct.toFixed(0); + console.log("Found CCT:", cct); + + // If we have both x and y values by now, let's try to update the plot again + const cieXStr = cieXDisplay.textContent; + const cieYStr = cieYDisplay.textContent; + if (cieXStr !== '-' && cieYStr !== '-') { + const cieX = parseFloat(cieXStr); + const cieY = parseFloat(cieYStr); + if (!isNaN(cieX) && !isNaN(cieY)) { + // Final attempt to update plot + updateCIEPlot(cieX, cieY); + console.log("Updating plot after CCT with:", cieX, cieY); + } + } + } +} + +// Initialize debug coordinates +document.addEventListener('DOMContentLoaded', function() { + debugCoordinates.textContent = 'Waiting for color data...'; +}); + +// Update the CIE plot with new data point +function updateCIEPlot(x, y) { + console.log(`Plotting CIE coordinates: x=${x}, y=${y}`); // Debug log + + // Get the dimensions of the CIE diagram container + const cieDiagram = document.getElementById('cie-diagram'); + + // Ensure we're only working with valid x,y coordinates + if (isNaN(x) || isNaN(y) || x < 0 || y < 0 || x > 1 || y > 1) { + console.warn(`Invalid CIE coordinates: x=${x}, y=${y}`); + debugCoordinates.textContent = `Invalid coordinates: x=${x}, y=${y}`; + return; + } + + // Adjust coordinates to fit the visible area of the CIE diagram + // CIE diagram typically has coordinates: x [0-0.8], y [0-0.9] + const xMax = 0.8; + const yMax = 0.9; + + // Get actual dimensions of the CIE diagram image + const cieImage = document.querySelector('#cie-diagram img'); + const imgWidth = cieImage.clientWidth; + const imgHeight = cieImage.clientHeight; + + // Calculate percentage positions within the SVG viewBox + const xPercent = (x / xMax) * 100; // Scale to percentage of max x (0.8) + const yPercent = (1 - (y / yMax)) * 100; // Invert y-axis and scale to percentage of max y (0.9) + + console.log(`Plotting at: left=${xPercent}%, top=${yPercent}%`); // Debug log + + // Set the data point position + dataPoint.style.left = `${xPercent}%`; + dataPoint.style.top = `${yPercent}%`; + dataPoint.style.display = 'block'; + + // Show debug coordinates for troubleshooting + debugCoordinates.textContent = `CIE: (${x.toFixed(4)}, ${y.toFixed(4)}) → Position: (${Math.round(xPercent)}%, ${Math.round(yPercent)}%)`; + + // 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'; + debugCoordinates.textContent = 'Waiting for color data...'; + 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 = ''; +}