Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/Software/ @jamespilgrim

# Python Web Server
/Software/web-server/ @connorgallopo
/Software/web-server/ @connorgallopo

# Yolo Model + Tooling
/Software/GroundTruthAnnotator/ @connorgallopo
Expand Down
40 changes: 39 additions & 1 deletion Software/web-server/static/css/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -291,4 +329,4 @@
.metric-value span:first-child {
font-size: 3.5rem;
}
}
}
2 changes: 1 addition & 1 deletion Software/web-server/static/js/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,4 @@ document.addEventListener('DOMContentLoaded', () => {

setInterval(checkSystemStatus, 5000);
setInterval(checkPiTracStatus, 5000);
});
});
220 changes: 176 additions & 44 deletions Software/web-server/static/js/dashboard.js
Original file line number Diff line number Diff line change
@@ -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`);
Expand Down Expand Up @@ -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) =>
`<img src="/images/${img}" alt="Shot ${idx + 1}" class="shot-image" loading="lazy" onclick="openImage('${img}')">`
).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 = '';
}
}
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -182,4 +314,4 @@ document.addEventListener('DOMContentLoaded', () => {
connectWebSocket();
}
});
});
});
12 changes: 11 additions & 1 deletion Software/web-server/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@

<div class="image-gallery">
<h2>Shot Images</h2>
<div class="ball-ready-image" id="ball-ready-image">
<img src="/images/log_ball_final_found_ball_img.png" alt="Ball Ready View" loading="lazy">
</div>
<div class="ball-hit-image" id="ball-hit-image">
<img
src="/images/ball_exposure_candidates.png"
data-base-src="/images/ball_exposure_candidates.png"
alt="Ball Exposure Candidate"
loading="lazy">
</div>
<div class="image-grid" id="image-grid">
{% for image in shot.images %}
<img src="/images/{{ image }}" alt="Shot {{ loop.index }}" class="shot-image" loading="lazy">
Expand All @@ -89,4 +99,4 @@ <h2>Shot Images</h2>

{% block extra_scripts %}
<script src="/static/js/dashboard.js"></script>
{% endblock %}
{% endblock %}