406 lines
21 KiB
HTML
406 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Pupilometer Unified Control</title>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<h1>Pupilometer Unified Control</h1>
|
|
<div class="main-container">
|
|
<!-- The content sections will be populated based on the view -->
|
|
<div id="lamp" class="content-section lamp-view">
|
|
<!-- Lamp Control UI goes here -->
|
|
<div class="container">
|
|
<h2>Lamp Matrix Control</h2>
|
|
<div class="region-control">
|
|
<label for="region-select">Select Region:</label>
|
|
<select id="region-select">
|
|
<option value="" disabled selected>-- Select a region --</option>
|
|
<option value="Upper">Upper</option>
|
|
<option value="Lower">Lower</option>
|
|
<option value="Left">Left</option>
|
|
<option value="Right">Right</option>
|
|
<option value="Inner ring">Inner ring</option>
|
|
<option value="Outer ring">Outer ring</option>
|
|
<option value="All">All</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<div class="matrix-grid">
|
|
{% for row in range(5) %}
|
|
{% for col in range(5) %}
|
|
<div class="lamp" data-row="{{ row }}" data-col="{{ col }}" style="background-color: {{ matrix[row][col] }};"></div>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="slider-controls">
|
|
<div class="center-lamp-control">
|
|
<h2>Center Lamp</h2>
|
|
<div class="slider-group center-slider-group">
|
|
<div class="slider-row">
|
|
<span class="slider-label">Warm White (3000K)</span>
|
|
<input type="range" id="center-ww-slider" min="0" max="255" value="0" class="white-3000k">
|
|
<div class="number-input-controls">
|
|
<button type="button" class="decrement-btn">-</button>
|
|
<input type="number" id="center-ww-number" min="0" max="255" value="0">
|
|
<button type="button" class="increment-btn">+</button>
|
|
</div>
|
|
</div>
|
|
<div class="slider-row">
|
|
<span class="slider-label">Cool White (6500K)</span>
|
|
<input type="range" id="center-cw-slider" min="0" max="255" value="0" class="white-6500k">
|
|
<div class="number-input-controls">
|
|
<button type="button" class="decrement-btn">-</button>
|
|
<input type="number" id="center-cw-number" min="0" max="255" value="0">
|
|
<button type="button" class="increment-btn">+</button>
|
|
</div>
|
|
</div>
|
|
<div class="slider-row">
|
|
<span class="slider-label">Blue</span>
|
|
<input type="range" id="center-blue-slider" min="0" max="255" value="0" class="blue">
|
|
<div class="number-input-controls">
|
|
<button type="button" class="decrement-btn">-</button>
|
|
<input type="number" id="center-blue-number" min="0" max="255" value="0">
|
|
<button type="button" class="increment-btn">+</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-panel">
|
|
<h2>Selected Region</h2>
|
|
<div class="slider-group region-slider-group">
|
|
<div class="slider-row">
|
|
<span class="slider-label">Warm White (3000K)</span>
|
|
<input type="range" id="ww-slider" min="0" max="255" value="0" class="white-3000k">
|
|
<div class="number-input-controls">
|
|
<button type="button" class="decrement-btn">-</button>
|
|
<input type="number" id="ww-number" min="0" max="255" value="0">
|
|
<button type="button" class="increment-btn">+</button>
|
|
</div>
|
|
</div>
|
|
<div class="slider-row">
|
|
<span class="slider-label">Cool White (6500K)</span>
|
|
<input type="range" id="cw-slider" min="0" max="255" value="0" class="white-6500k">
|
|
<div class="number-input-controls">
|
|
<button type="button" class="decrement-btn">-</button>
|
|
<input type="number" id="cw-number" min="0" max="255" value="0">
|
|
<button type="button" class="increment-btn">+</button>
|
|
</div>
|
|
</div>
|
|
<div class="slider-row">
|
|
<span class="slider-label">Blue</span>
|
|
<input type="range" id="blue-slider" min="0" max="255" value="0" class="blue">
|
|
<div class="number-input-controls">
|
|
<button type="button" class="decrement-btn">-</button>
|
|
<input type="number" id="blue-number" min="0" max="255" value="0">
|
|
<button type="button" class="increment-btn">+</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="camera" class="content-section camera-view">
|
|
<h2>Basler Final Feed</h2>
|
|
<div class="camera-streams-grid">
|
|
<div class="camera-color-row">
|
|
{% for cam_index in range(detected_cams_info|length) %}
|
|
{% set cam_info = detected_cams_info[cam_index] %}
|
|
{% if cam_info.is_color %}
|
|
<div class="camera-container-individual {% if cam_info.is_color %}camera-color{% else %}camera-mono{% endif %}" style="--aspect-ratio: {{ cam_info.aspect_ratio }};">
|
|
<img src="{{ url_for('video_feed', stream_id=cam_index) }}" class="camera-stream-individual">
|
|
<div class="camera-label">{{ cam_info.model }} ({{ 'Color' if cam_info.is_color else 'Mono' }})</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
<div class="camera-mono-row">
|
|
{% for cam_index in range(detected_cams_info|length) %}
|
|
{% set cam_info = detected_cams_info[cam_index] %}
|
|
{% if not cam_info.is_color %}
|
|
<div class="camera-container-individual {% if cam_info.is_color %}camera-color{% else %}camera-mono{% endif %}" style="--aspect-ratio: {{ cam_info.aspect_ratio }};">
|
|
<img src="{{ url_for('video_feed', stream_id=cam_index) }}" class="camera-stream-individual">
|
|
<div class="camera-label">{{ cam_info.model }} ({{ 'Color' if cam_info.is_color else 'Mono' }})</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="hud" id="fps-counter">FPS: --</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// FPS counter
|
|
setInterval(function() {
|
|
fetch('/get_fps').then(r => r.json()).then(d => {
|
|
document.getElementById('fps-counter').innerText = "FPS: " + d.fps;
|
|
});
|
|
}, 500);
|
|
|
|
// State for the entire 5x5 matrix, storing {ww, cw, blue} for each lamp
|
|
var lampMatrixState = Array(5).fill(null).map(() => Array(5).fill({ww: 0, cw: 0, blue: 0}));
|
|
var selectedLamps = [];
|
|
|
|
// Function to calculate a visual RGB color from the three light values using a proper additive model
|
|
function calculateRgb(ww, cw, blue) {
|
|
const warmWhiteR = 255, warmWhiteG = 192, warmWhiteB = 128;
|
|
const coolWhiteR = 192, coolWhiteG = 224, coolWhiteB = 255;
|
|
const blueR = 0, blueG = 0, blueB = 255;
|
|
var r = (ww / 255) * warmWhiteR + (cw / 255) * coolWhiteR + (blue / 255) * blueR;
|
|
var g = (ww / 255) * warmWhiteG + (cw / 255) * coolWhiteR + (blue / 255) * blueG;
|
|
var b = (ww / 255) * warmWhiteB + (cw / 255) * coolWhiteB + (blue / 255) * blueB;
|
|
r = Math.min(255, Math.round(r));
|
|
g = Math.min(255, Math.round(g));
|
|
b = Math.min(255, Math.round(b));
|
|
var toHex = (c) => ('0' + c.toString(16)).slice(-2);
|
|
return '#' + toHex(r) + toHex(g) + toHex(b);
|
|
}
|
|
|
|
function updateLampUI(lamp, colorState) {
|
|
var newColor = calculateRgb(colorState.ww, colorState.cw, colorState.blue);
|
|
var lampElement = $(`.lamp[data-row="${lamp.row}"][data-col="${lamp.col}"]`);
|
|
lampElement.css('background-color', newColor);
|
|
if (newColor === '#000000') {
|
|
lampElement.removeClass('on');
|
|
lampElement.css('box-shadow', `inset 0 0 5px rgba(0,0,0,0.5)`);
|
|
} else {
|
|
lampElement.addClass('on');
|
|
lampElement.css('box-shadow', `0 0 15px ${newColor}, 0 0 25px ${newColor}`);
|
|
}
|
|
}
|
|
|
|
function sendFullMatrixUpdate(lampsToUpdate, isRegionUpdate = false) {
|
|
var fullMatrixData = lampMatrixState.map(row => row.map(lamp => ({
|
|
ww: lamp.ww,
|
|
cw: lamp.cw,
|
|
blue: lamp.blue
|
|
})));
|
|
|
|
$.ajax({
|
|
url: '/set_matrix',
|
|
type: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify({ matrix: fullMatrixData }),
|
|
success: function(response) {
|
|
if (response.success) {
|
|
if (isRegionUpdate) {
|
|
for (var r = 0; r < 5; r++) {
|
|
for (var c = 0; c < 5; c++) {
|
|
updateLampUI({row: r, col: c}, lampMatrixState[r][c]);
|
|
}
|
|
}
|
|
} else {
|
|
lampsToUpdate.forEach(function(lamp) {
|
|
updateLampUI(lamp, lampMatrixState[lamp.row][lamp.col]);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateSliders(ww, cw, blue, prefix = '') {
|
|
$(`#${prefix}ww-slider`).val(ww);
|
|
$(`#${prefix}cw-slider`).val(cw);
|
|
$(`#${prefix}blue-slider`).val(blue);
|
|
$(`#${prefix}ww-number`).val(ww);
|
|
$(`#${prefix}cw-number`).val(cw);
|
|
$(`#${prefix}blue-number`).val(blue);
|
|
}
|
|
|
|
$(document).ready(function() {
|
|
var regionMaps = {
|
|
'Upper': [
|
|
{row: 0, col: 0}, {row: 0, col: 1}, {row: 0, col: 2}, {row: 0, col: 3}, {row: 0, col: 4},
|
|
{row: 1, col: 0}, {row: 1, col: 1}, {row: 1, col: 2}, {row: 1, col: 3}, {row: 1, col: 4},
|
|
],
|
|
'Lower': [
|
|
{row: 3, col: 0}, {row: 3, col: 1}, {row: 3, col: 2}, {row: 3, col: 3}, {row: 3, col: 4},
|
|
{row: 4, col: 0}, {row: 4, col: 1}, {row: 4, col: 2}, {row: 4, col: 3}, {row: 4, col: 4},
|
|
],
|
|
'Left': [
|
|
{row: 0, col: 0}, {row: 1, col: 0}, {row: 2, col: 0}, {row: 3, col: 0}, {row: 4, col: 0},
|
|
{row: 0, col: 1}, {row: 1, col: 1}, {row: 2, col: 1}, {row: 3, col: 1}, {row: 4, col: 1},
|
|
],
|
|
'Right': [
|
|
{row: 0, col: 3}, {row: 1, col: 3}, {row: 2, col: 3}, {row: 3, col: 3}, {row: 4, col: 3},
|
|
{row: 0, col: 4}, {row: 1, col: 4}, {row: 2, col: 4}, {row: 3, col: 4}, {row: 4, col: 4},
|
|
],
|
|
'Inner ring': [
|
|
{row: 1, col: 1}, {row: 1, col: 2}, {row: 1, col: 3},
|
|
{row: 2, col: 1}, {row: 2, col: 3},
|
|
{row: 3, col: 1}, {row: 3, col: 2}, {row: 3, col: 3}
|
|
],
|
|
'Outer ring': [
|
|
{row: 0, col: 0}, {row: 0, col: 1}, {row: 0, col: 2}, {row: 0, col: 3}, {row: 0, col: 4},
|
|
{row: 1, col: 0}, {row: 1, col: 4},
|
|
{row: 2, col: 0}, {row: 2, col: 4},
|
|
{row: 3, col: 0}, {row: 3, col: 4},
|
|
{row: 4, col: 0}, {row: 4, col: 1}, {row: 4, col: 2}, {row: 4, col: 3}, {row: 4, col: 4},
|
|
],
|
|
'All': [
|
|
{row: 0, col: 0}, {row: 0, col: 1}, {row: 0, col: 2}, {row: 0, col: 3}, {row: 0, col: 4},
|
|
{row: 1, col: 0}, {row: 1, col: 1}, {row: 1, col: 2}, {row: 1, col: 3}, {row: 1, col: 4},
|
|
{row: 2, col: 0}, {row: 2, col: 1}, {row: 2, col: 3}, {row: 2, col: 4},
|
|
{row: 3, col: 0}, {row: 3, col: 1}, {row: 3, col: 2}, {row: 3, col: 3}, {row: 3, col: 4},
|
|
{row: 4, col: 0}, {row: 4, col: 1}, {row: 4, col: 2}, {row: 4, col: 3}, {row: 4, col: 4},
|
|
]
|
|
};
|
|
|
|
var allRegionWithoutCenter = regionMaps['All'].filter(lamp => !(lamp.row === 2 && lamp.col === 2));
|
|
regionMaps['All'] = allRegionWithoutCenter;
|
|
|
|
$('.lamp').each(function() {
|
|
var row = $(this).data('row');
|
|
var col = $(this).data('col');
|
|
var color = $(this).css('background-color');
|
|
var rgb = color.match(/\d+/g);
|
|
lampMatrixState[row][col] = {
|
|
ww: rgb[0], cw: rgb[1], blue: rgb[2]
|
|
};
|
|
});
|
|
|
|
$('#region-select').on('change', function() {
|
|
var region = $(this).val();
|
|
|
|
if (region) {
|
|
$('.control-panel').removeClass('inactive-control');
|
|
} else {
|
|
$('.control-panel').addClass('inactive-control');
|
|
}
|
|
|
|
var newlySelectedLamps = regionMaps[region];
|
|
$('.lamp').removeClass('selected');
|
|
var ww = parseInt($('#ww-slider').val());
|
|
var cw = parseInt($('#cw-slider').val());
|
|
var blue = parseInt($('#blue-slider').val());
|
|
|
|
var lampsToUpdate = [];
|
|
var centerLampState = lampMatrixState[2][2];
|
|
lampMatrixState = Array(5).fill(null).map(() => Array(5).fill({ww: 0, cw: 0, blue: 0}));
|
|
lampMatrixState[2][2] = centerLampState;
|
|
|
|
selectedLamps = newlySelectedLamps;
|
|
selectedLamps.forEach(function(lamp) {
|
|
$(`.lamp[data-row="${lamp.row}"][data-col="${lamp.col}"]`).addClass('selected');
|
|
lampMatrixState[lamp.row][lamp.col] = {ww: ww, cw: cw, blue: blue};
|
|
});
|
|
|
|
if (selectedLamps.length > 0) {
|
|
var firstLamp = selectedLamps[0];
|
|
var firstLampState = lampMatrixState[firstLamp.row][firstLamp.col];
|
|
updateSliders(firstLampState.ww, firstLampState.cw, firstLampState.blue, '');
|
|
}
|
|
|
|
sendFullMatrixUpdate(lampsToUpdate, true);
|
|
});
|
|
|
|
$('.region-slider-group input').on('input', function() {
|
|
if (selectedLamps.length === 0) return;
|
|
var target = $(this);
|
|
var originalVal = target.val();
|
|
var value = parseInt(originalVal, 10);
|
|
if (isNaN(value) || value < 0) { value = 0; }
|
|
if (value > 255) { value = 255; }
|
|
if (target.is('[type="number"]') && value.toString() !== originalVal) {
|
|
target.val(value);
|
|
}
|
|
var id = target.attr('id');
|
|
if (target.is('[type="range"]')) {
|
|
$(`#${id.replace('-slider', '-number')}`).val(value);
|
|
} else if (target.is('[type="number"]')) {
|
|
$(`#${id.replace('-number', '-slider')}`).val(value);
|
|
}
|
|
var ww = parseInt($('#ww-slider').val());
|
|
var cw = parseInt($('#cw-slider').val());
|
|
var blue = parseInt($('#blue-slider').val());
|
|
var lampsToUpdate = [];
|
|
selectedLamps.forEach(function(lamp) {
|
|
lampMatrixState[lamp.row][lamp.col] = {ww: ww, cw: cw, blue: blue};
|
|
lampsToUpdate.push(lamp);
|
|
});
|
|
sendFullMatrixUpdate(lampsToUpdate);
|
|
});
|
|
|
|
$('.center-slider-group input').on('input', function() {
|
|
var target = $(this);
|
|
var originalVal = target.val();
|
|
var value = parseInt(originalVal, 10);
|
|
if (isNaN(value) || value < 0) { value = 0; }
|
|
if (value > 255) { value = 255; }
|
|
if (target.is('[type="number"]') && value.toString() !== originalVal) {
|
|
target.val(value);
|
|
}
|
|
var id = target.attr('id');
|
|
if (target.is('[type="range"]')) {
|
|
$(`#${id.replace('-slider', '-number')}`).val(value);
|
|
} else if (target.is('[type="number"]')) {
|
|
$(`#${id.replace('-number', '-slider')}`).val(value);
|
|
}
|
|
var ww = parseInt($('#center-ww-slider').val());
|
|
var cw = parseInt($('#center-cw-slider').val());
|
|
var blue = parseInt($('#center-blue-slider').val());
|
|
var centerLamp = {row: 2, col: 2};
|
|
lampMatrixState[centerLamp.row][centerLamp.col] = {ww: ww, cw: cw, blue: blue};
|
|
sendFullMatrixUpdate([centerLamp]);
|
|
});
|
|
|
|
// Handle increment/decrement buttons
|
|
$('.number-input-controls button').on('click', function() {
|
|
var btn = $(this);
|
|
var numberInput = btn.siblings('input[type="number"]');
|
|
var currentVal = parseInt(numberInput.val());
|
|
var min = parseInt(numberInput.attr('min'));
|
|
var max = parseInt(numberInput.attr('max'));
|
|
|
|
if (btn.hasClass('decrement-btn')) {
|
|
currentVal = Math.max(min, currentVal - 1);
|
|
} else if (btn.hasClass('increment-btn')) {
|
|
currentVal = Math.min(max, currentVal + 1);
|
|
}
|
|
|
|
numberInput.val(currentVal);
|
|
// Trigger the 'input' event to propagate the change to the slider and matrix update logic
|
|
numberInput.trigger('input');
|
|
});
|
|
|
|
|
|
if (!$('#region-select').val()) {
|
|
$('.control-panel').addClass('inactive-control');
|
|
}
|
|
|
|
// Mobile tab handling
|
|
if (window.innerWidth <= 768) {
|
|
// Dynamically add tab buttons
|
|
const tabsDiv = $('<div class="tabs"></div>');
|
|
tabsDiv.append('<button class="tab-link" data-tab="camera">Camera</button>');
|
|
tabsDiv.append('<button class="tab-link" data-tab="lamp">Lamp Control</button>');
|
|
// Prepend tabsDiv to .main-container
|
|
$('.main-container').prepend(tabsDiv);
|
|
|
|
// Hide all content sections initially
|
|
$('.content-section').hide();
|
|
// Show the camera section by default
|
|
$('#camera').show();
|
|
// Make the Camera tab active
|
|
$('.tab-link[data-tab="camera"]').addClass('active');
|
|
|
|
// Add click handlers for tab buttons
|
|
$('.tab-link').on('click', function() {
|
|
$('.tab-link').removeClass('active');
|
|
$(this).addClass('active');
|
|
$('.content-section').hide();
|
|
$(`#${$(this).data('tab')}`).show();
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |