websocket server example
This commit is contained in:
parent
40bd717d32
commit
341a4de061
6 changed files with 560 additions and 0 deletions
3
LICENSES/LicenseRef-Wikipedia-Public-Domain.txt
Normal file
3
LICENSES/LicenseRef-Wikipedia-Public-Domain.txt
Normal file
|
|
@ -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.
|
||||
79
examples/opt4048_websocket.py
Normal file
79
examples/opt4048_websocket.py
Normal file
|
|
@ -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())
|
||||
3
examples/opt4048_ws_static/cie1931_diagram.svg
Normal file
3
examples/opt4048_ws_static/cie1931_diagram.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.5 KiB |
2
examples/opt4048_ws_static/cie1931_diagram.svg.license
Normal file
2
examples/opt4048_ws_static/cie1931_diagram.svg.license
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2009 BenRG
|
||||
// SPDX-License-Identifier: LicenseRef-Wikipedia-Public-Domain
|
||||
202
examples/opt4048_ws_static/index.html
Normal file
202
examples/opt4048_ws_static/index.html
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: Copyright (c) 2025 Tim C for Adafruit Industries
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
<!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: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 8px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
.controls {
|
||||
flex: 1;
|
||||
min-width: 230px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.visualization {
|
||||
flex: 2;
|
||||
min-width: 280px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#cie-diagram {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
margin: 0 auto 10px auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#cie-diagram img {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
#data-point {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
border: 3px solid black;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.9);
|
||||
z-index: 10;
|
||||
}
|
||||
#color-sample {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid #ccc;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
}
|
||||
#serial-log {
|
||||
flex: 1;
|
||||
min-height: 80px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
margin: 4px 4px 4px 0;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.data-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.data-box {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.data-box h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.data-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for very small screens */
|
||||
@media (max-height: 600px) {
|
||||
h1 { font-size: 20px !important; margin: 5px 0 !important; }
|
||||
h2 { font-size: 16px; margin: 8px 0 5px 0; }
|
||||
p { font-size: 12px !important; margin: 5px 0 !important; }
|
||||
.data-box { padding: 5px; }
|
||||
.data-box h3 { font-size: 12px; margin: 0 0 3px 0; }
|
||||
.data-value { font-size: 14px; }
|
||||
#serial-log { min-height: 60px; max-height: 100px; }
|
||||
button { padding: 6px 10px; font-size: 12px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div class="controls">
|
||||
<h2 style="margin: 10px 0; font-size: 24px;">OPT4048 CIE Color Plotter</h2>
|
||||
<p style="margin: 8px 0; font-size: 14px;">Connect your Arduino with OPT4048 sensor to visualize color measurements on a CIE diagram.</p>
|
||||
|
||||
<button id="clear-button">Clear Log</button>
|
||||
<button id="test-plot-button" style="display: none;">Test Plot Point</button>
|
||||
|
||||
<h2>Connection Status</h2>
|
||||
<p id="status">Not connected</p>
|
||||
|
||||
<h2 style="margin-bottom: 5px;">Serial Monitor</h2>
|
||||
<div id="serial-log"></div>
|
||||
</div>
|
||||
|
||||
<div class="visualization">
|
||||
<h2 style="margin: 10px 0; font-size: 24px;">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>
|
||||
<!-- Debug info: will show exact location of measured color -->
|
||||
<div id="debug-coordinates" style="position: absolute; bottom: 3px; right: 3px; font-size: 10px; background: rgba(255,255,255,0.7); padding: 2px; border: 1px solid #ccc; display: block;"></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>
|
||||
271
examples/opt4048_ws_static/script.js
Normal file
271
examples/opt4048_ws_static/script.js
Normal file
|
|
@ -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 = '';
|
||||
}
|
||||
Loading…
Reference in a new issue