diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5114bafe..c70baac6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,7 +9,7 @@ /Software/ @jamespilgrim # Python Web Server -/Software/web-server/ @connorgallopo +/Software/web-server/ @connorgallopo # Yolo Model + Tooling /Software/GroundTruthAnnotator/ @connorgallopo diff --git a/Software/web-server/static/css/dashboard.css b/Software/web-server/static/css/dashboard.css index c059549a..b6045364 100644 --- a/Software/web-server/static/css/dashboard.css +++ b/Software/web-server/static/css/dashboard.css @@ -111,6 +111,44 @@ font-size: clamp(1.25rem, 3vw, 1.5rem); } +.ball-ready-image { + display: none; + margin-bottom: 1rem; + text-align: center; +} + +.ball-ready-image.visible { + display: block; +} + +.ball-ready-image img { + max-width: 320px; + width: 100%; + height: auto; + border-radius: 0.5rem; + box-shadow: var(--shadow-sm); + border: 1px solid var(--border-color); +} + +.ball-hit-image { + display: none; + margin-bottom: 1rem; + text-align: center; +} + +.ball-hit-image.visible { + display: block; +} + +.ball-hit-image img { + width: 100%; + max-width: 640px; + height: auto; + border-radius: 0.5rem; + box-shadow: var(--shadow-sm); + border: 1px solid var(--border-color); +} + .image-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); @@ -291,4 +329,4 @@ .metric-value span:first-child { font-size: 3.5rem; } -} \ No newline at end of file +} diff --git a/Software/web-server/static/js/common.js b/Software/web-server/static/js/common.js index afdaab9e..08eeea31 100644 --- a/Software/web-server/static/js/common.js +++ b/Software/web-server/static/js/common.js @@ -263,4 +263,4 @@ document.addEventListener('DOMContentLoaded', () => { setInterval(checkSystemStatus, 5000); setInterval(checkPiTracStatus, 5000); -}); \ No newline at end of file +}); diff --git a/Software/web-server/static/js/dashboard.js b/Software/web-server/static/js/dashboard.js index d14dae62..0211720d 100644 --- a/Software/web-server/static/js/dashboard.js +++ b/Software/web-server/static/js/dashboard.js @@ -1,6 +1,148 @@ // Dashboard-specific functionality (theme and dropdown handled by common.js) let ws = null; +const normalizeText = (value) => (value || '').toLowerCase(); + +const BALL_STATUS_RULES = [ + { + className: 'initializing', + title: 'System Initializing', + defaultMessage: 'Starting up PiTrac system...', + match: ({ type }) => type.includes('initializing'), + }, + { + className: 'waiting', + title: 'Waiting for Ball', + defaultMessage: 'Please place ball on tee', + match: ({ type }) => type.includes('waiting for ball'), + }, + { + className: 'waiting', + title: 'Waiting for Simulator', + defaultMessage: 'Waiting for simulator to be ready', + match: ({ type }) => type.includes('waiting for simulator'), + }, + { + className: 'stabilizing', + title: 'Ball Detected', + defaultMessage: 'Waiting for ball to stabilize...', + match: ({ type }) => type.includes('pausing') || type.includes('stabilization'), + }, + { + className: 'ready', + title: 'Ready to Hit!', + defaultMessage: 'Ball is ready - take your shot!', + match: ({ type, message }) => + type.includes('ball ready') || + type.includes('ready') || + type.includes('ball placed') || + message.includes("let's golf"), + }, + { + className: 'hit', + title: 'Ball Hit!', + defaultMessage: 'Processing shot data...', + match: ({ type }) => type.includes('hit'), + }, + { + className: 'error', + title: 'Error', + defaultMessage: 'An error occurred', + match: ({ type }) => type.includes('error'), + }, + { + className: 'error', + title: 'Multiple Balls Detected', + defaultMessage: 'Please remove extra balls', + match: ({ type }) => type.includes('multiple balls'), + }, +]; + +const HIT_IMAGE_NAME = 'ball_exposure_candidates.png'; +const HIT_IMAGE_URL = `/images/${HIT_IMAGE_NAME}`; +let hitImageTimer = null; + +const setBallReadyImageVisible = (isVisible) => { + const container = document.getElementById('ball-ready-image'); + if (!container) { + return; + } + if (isVisible) { + container.classList.add('visible'); + } else { + container.classList.remove('visible'); + } +}; + +const clearShotImages = () => { + const imageGrid = document.getElementById('image-grid'); + if (imageGrid) { + imageGrid.innerHTML = ''; + } +}; + +const hideBallHitImage = () => { + if (hitImageTimer) { + clearTimeout(hitImageTimer); + hitImageTimer = null; + } + + const container = document.getElementById('ball-hit-image'); + if (!container) { + return; + } + container.classList.remove('visible'); + const img = container.querySelector('img'); + if (img) { + const baseSrc = img.dataset.baseSrc || HIT_IMAGE_URL; + img.src = baseSrc; + img.onload = null; + img.onerror = null; + } +}; + +const resolveImagePath = (path) => { + if (!path) { + return null; + } + if (path.startsWith('http://') || path.startsWith('https://')) { + return path; + } + if (path.startsWith('/')) { + return path; + } + return `/images/${path.replace(/^\/+/, '')}`; +}; + +const showBallHitImage = (overridePath = null) => { + const container = document.getElementById('ball-hit-image'); + const img = container ? container.querySelector('img') : null; + + if (!container || !img) { + return; + } + + const cleanup = () => { + img.onload = null; + img.onerror = null; + hitImageTimer = null; + }; + + img.onload = () => { + cleanup(); + container.classList.add('visible'); + }; + + img.onerror = () => { + cleanup(); + hideBallHitImage(); + clearShotImages(); + }; + + const baseSrc = resolveImagePath(overridePath) || img.dataset.baseSrc || HIT_IMAGE_URL; + img.src = `${baseSrc}?t=${Date.now()}`; +}; + function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${protocol}//${window.location.host}/ws`); @@ -56,14 +198,14 @@ function updateDisplay(data) { // Update images - only show images for actual hits, clear for status messages const imageGrid = document.getElementById('image-grid'); - const resultType = (data.result_type || '').toLowerCase(); - - // Only show images for hit results - if (resultType.includes('hit') && data.images && data.images.length > 0) { - imageGrid.innerHTML = data.images.map((img, idx) => - `Shot ${idx + 1}` - ).join(''); - } else if (!resultType.includes('hit')) { + const resultType = normalizeText(data.result_type); + + if (resultType.includes('hit')) { + imageGrid.innerHTML = ''; + } else if (resultType.includes('ready')) { + imageGrid.innerHTML = ''; + hideBallHitImage(); + } else { imageGrid.innerHTML = ''; } } @@ -74,53 +216,43 @@ function updateBallStatus(resultType, message, isPiTracRunning) { const statusMessage = document.getElementById('ball-status-message'); indicator.classList.remove('initializing', 'waiting', 'stabilizing', 'ready', 'hit', 'error'); + setBallReadyImageVisible(false); if (isPiTracRunning === false) { indicator.classList.add('error'); statusTitle.textContent = 'System Stopped'; - statusMessage.textContent = 'PiTrac is not running - click Start to begin'; + statusMessage.textContent = 'Start PiTrac...'; + hideBallHitImage(); + clearShotImages(); return; } if (resultType) { - const normalizedType = resultType.toLowerCase(); - - if (normalizedType.includes('initializing')) { - indicator.classList.add('initializing'); - statusTitle.textContent = 'System Initializing'; - statusMessage.textContent = message || 'Starting up PiTrac system...'; - } else if (normalizedType.includes('waiting for ball')) { - indicator.classList.add('waiting'); - statusTitle.textContent = 'Waiting for Ball'; - statusMessage.textContent = message || 'Please place ball on tee'; - } else if (normalizedType.includes('waiting for simulator')) { - indicator.classList.add('waiting'); - statusTitle.textContent = 'Waiting for Simulator'; - statusMessage.textContent = message || 'Waiting for simulator to be ready'; - } else if (normalizedType.includes('pausing') || normalizedType.includes('stabilization')) { - indicator.classList.add('stabilizing'); - statusTitle.textContent = 'Ball Detected'; - statusMessage.textContent = message || 'Waiting for ball to stabilize...'; - } else if (normalizedType.includes('ball ready') || normalizedType.includes('ready')) { - indicator.classList.add('ready'); - statusTitle.textContent = 'Ready to Hit!'; - statusMessage.textContent = message || 'Ball is ready - take your shot!'; - } else if (normalizedType.includes('hit')) { - indicator.classList.add('hit'); - statusTitle.textContent = 'Ball Hit!'; - statusMessage.textContent = message || 'Processing shot data...'; - } else if (normalizedType.includes('error')) { - indicator.classList.add('error'); - statusTitle.textContent = 'Error'; - statusMessage.textContent = message || 'An error occurred'; - } else if (normalizedType.includes('multiple balls')) { - indicator.classList.add('error'); - statusTitle.textContent = 'Multiple Balls Detected'; - statusMessage.textContent = message || 'Please remove extra balls'; + const normalizedType = normalizeText(resultType); + const normalizedMessage = normalizeText(message); + const statusContext = { type: normalizedType, message: normalizedMessage }; + const rule = BALL_STATUS_RULES.find((r) => r.match(statusContext)); + + if (rule) { + indicator.classList.add(rule.className); + statusTitle.textContent = rule.title; + statusMessage.textContent = message || rule.defaultMessage; + setBallReadyImageVisible(rule.className === 'ready'); + + if (rule.className === 'ready') { + hideBallHitImage(); + } else if (rule.className === 'hit') { + if (hitImageTimer) { + clearTimeout(hitImageTimer); + } + hitImageTimer = setTimeout(() => showBallHitImage(), 2000); + } } else { statusTitle.textContent = 'System Status'; statusMessage.textContent = message || resultType; } + } else { + setBallReadyImageVisible(false); } } @@ -182,4 +314,4 @@ document.addEventListener('DOMContentLoaded', () => { connectWebSocket(); } }); -}); \ No newline at end of file +}); diff --git a/Software/web-server/templates/dashboard.html b/Software/web-server/templates/dashboard.html index 4c696003..fc6ab76a 100644 --- a/Software/web-server/templates/dashboard.html +++ b/Software/web-server/templates/dashboard.html @@ -74,6 +74,16 @@