+
+
+
Accelerometer Data (m/s2)
+ Info +
Live visualization of raw accelerometer data (X, Y, Z axes) from the board's Movement module. Horizontal axis represents time; vertical axis shows acceleration in m/s2
+
+
+ +
+
+
+ X +
+
+
+ Y +
+
+
+ Z +
+
+
+
+ No data +

No data

+
+
+ +
+
+
+
+
+
+ + Info +
It quantifies how far the current vibration is from the model's 'normal' clusters.
+ + Scores above your threshold are flagged as an anomaly.
+
+ +
+
+ 0 +
+
+
0.5
+ +
+ 1 +
+
+
+
+ +
+ +
-
- -
- -
- No anomaly +
+
+
+

Recent Anomalies

+
+
    +
    +
    diff --git a/examples/vibration-anomaly-detection/assets/style.css b/examples/vibration-anomaly-detection/assets/style.css index 9d37cdc..b1da1c2 100644 --- a/examples/vibration-anomaly-detection/assets/style.css +++ b/examples/vibration-anomaly-detection/assets/style.css @@ -4,20 +4,22 @@ * SPDX-License-Identifier: MPL-2.0 */ -@import url("fonts/roboto-mono.css"); +@import url("fonts/fonts.css"); /* * This CSS is used to center the various elements on the screen */ * { + box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; - background-color: #ECF1F1; - color: #2C353A; + background-color: #DAE3E3; + line-height: 1.6; + color: #343a40; padding: 24px 40px; } @@ -25,18 +27,19 @@ body { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 32px; + margin-bottom: 24px; + padding: 12px 0; } .arduino-text { color: #008184; font-family: "Roboto Mono", monospace; font-size: 20px; - font-weight: 600; + font-weight: 700; margin: 0; font-style: normal; line-height: 170%; - letter-spacing: 0.28px; + letter-spacing: 2.4px; } .arduino-logo { @@ -44,84 +47,397 @@ body { width: auto; } -.container { - text-align: center; +.main-content { + display: flex; + gap: 30px; + align-items: stretch; } /* - * LED Button styling + * Styles for specific components required by Anomaly Detection */ -.led-container { + +.legend { display: flex; - justify-content: center; - margin-bottom: 32px; - padding-top: 40px; + gap: 16px; + margin-top: 8px; + font-size: 14px; } -#fan-led { - width: 128px; - height: 128px; - border-radius: 50%; - cursor: pointer; - transition: all 0.3s ease; +.legend-item { display: flex; align-items: center; - justify-content: center; - font-family: inherit; - font-weight: 600; - font-size: 14px; - text-align: center; - line-height: 1.2; - outline: none; - position: relative; - border: 2px solid #C9D2D2; + gap: 6px; } -#fan-led.led-off { - background: #008184; - color: #ffffff; - box-shadow: 0 0 20px #008184, 0 0 40px #008184, 0 0 60px #008184; - border-color: #008184; +.legend-color { + width: 12px; + height: 4px; + border-radius: 1px; } -#fan-led.led-on { - background: #e00d0d; - color: #ffffff; - box-shadow: 0 0 20px #e00d0d, 0 0 40px #e00d0d, 0 0 60px #e00d0d; - border-color: #e00d0d; +.controls-section-right { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; } -#fan-led:hover { - transform: scale(1.05); +.box-title { + color: #2C353A; + font-family: "Roboto Mono"; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 170%; + letter-spacing: 1.2px; + margin-bottom: 16px; } -#fan-led:active { - transform: scale(0.95); +.controls-section-left { + background: #ECF1F1; + padding: 16px; + border-radius: 8px; + min-width: 750px; + width: 100%; + display: flex; + flex-direction: column; } -.instruction-text { - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 160%; - letter-spacing: 0.12px; +.right-column { + display: flex; + flex-direction: column; + gap: 32px; + width: 100%; + max-width: 550px; +} + +.right-column .container:last-child { + flex-grow: 1; +} + +.container-right { + background: #ECF1F1; + padding: 16px; + border-radius: 8px; +} + +.error-message { + margin-top: 20px; + padding: 10px; + border-radius: 5px; + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.recent-scans-title-container { + display: flex; + align-items: center; + gap: 8px; + justify-content: space-between; +} + +.recent-scans-title { color: #2C353A; + font-family: "Roboto Mono", monospace; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 170%; + letter-spacing: 1.2px; + margin: 0; +} + +#recentClassifications { + list-style-type: none; + padding: 0; + flex: 1; } /* * Responsive design */ @media (max-width: 768px) { - body { - padding: 12px 20px; + .main-content { + flex-direction: column; } .arduino-text { font-size: 14px; } + .container { + padding: 15px; + } + + .controls-section-left { + min-width: 330px; + } + .arduino-logo { - height: 20px; + height: 16px; width: auto; } + +} + +@media (max-width: 1024px) and (min-width: 769px) { + .controls-section-left { + min-width: 490px; + } +} + +@media (max-width: 480px) { + .controls-section-left { + min-width: 170px; + } +} + +.info-btn { + width: 14px; + height: 14px; + cursor: pointer; + border-radius: 50%; + background-color: #C9D2D2; + padding: 2px; + transition: background 0.2s; + position: relative; +} + +.popover { + position: absolute; + left: 5%; + top: 70%; + margin-left: 8px; + display: none; + background: #fff; + padding: 16px 24px; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + z-index: 10; + width: 300px; + color: #2C353A; + font-weight: 100; + font-family: "Open Sans"; + font-size: 12px; + line-height: 170%; + letter-spacing: 0.12px; +} + +.popover.active { + display: block; +} + +.no-recent-anomalies { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #5D6A6B; + gap: 8px; + margin: auto; +} + +.no-recent-anomalies p { + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 19.2px */ + letter-spacing: 0.12px; +} + +.feedback-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; +} + +.feedback-text { + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 19.2px */ + letter-spacing: 0.12px; +} + +.no-data-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #5D6A6B; + gap: 8px; + margin: auto; +} + +.no-data-placeholder p { + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 19.2px */ + letter-spacing: 0.12px; +} + +.control-group { + position: relative; +} + +.slider-box { + display: flex; + align-items: center; + gap: 10px; +} + +.control-confidence { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +#confidenceSlider { + width: 100%; + height: 6px; + border-radius: 3px; + background: #DAE3E3; + outline: none; + -webkit-appearance: none; + appearance: none; + position: relative; + margin: 20px 0 10px 0; +} + +#confidenceSlider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: #008184; + cursor: pointer; + position: relative; + bottom: 3px; + z-index: 2; +} + +#confidenceSlider::-webkit-slider-runnable-track { + width: 100%; + height: 6px; + border-radius: 3px; + background: #DAE3E3; +} + +#confidenceSlider::-moz-range-track { + width: 100%; + height: 6px; + border-radius: 3px; + background: #DAE3E3; + border: none; +} + +.slider-container { + position: relative; + width: 100%; +} + +.slider-progress { + position: absolute; + top: 20px; + left: 0; + height: 6px; + background: #008184; + border-radius: 3px; + pointer-events: none; + z-index: 1; + transition: width 0.1s ease; +} + +.confidence-value-display { + position: absolute; + top: -3px; + transform: translateX(-50%); + color: #008184; + padding: 2px 6px; + pointer-events: none; + z-index: 3; + white-space: nowrap; + transition: left 0.1s ease; + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 160%; + letter-spacing: 0.12px; +} + +.confidence-limits { + color: #2C353A; + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 160%; + letter-spacing: 0.12px; + margin-top: 10px; +} + +.btn-tertiary { + border-radius: 6px; + border: 1px solid #C9D2D2; + background: white; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 4px 8px; + cursor: pointer; + transition: all 0.3s ease; + font-size: 12px; + min-width: 50px; + height: 36px; +} + +.confidence-input { + border: none; + background: transparent; + font-size: 12px; + font-weight: inherit; + color: inherit; + text-align: center; + width: 40px; + padding: 0; + margin: 0; + outline: none; + cursor: text; +} + +.confidence-input:focus { + background: rgba(0, 129, 132, 0.1); + border-radius: 2px; +} + +.reset-icon { + width: 18px; + height: 18px; + opacity: 0.7; + transition: opacity 0.3s ease; + cursor: pointer; +} + +.reset-icon svg path { + fill: black; +} + +.btn-tertiary:hover .reset-icon { + opacity: 1; } \ No newline at end of file From a7acda947adfb8baea4a057924ad2d86e3f59825 Mon Sep 17 00:00:00 2001 From: Matteo Marsala Date: Mon, 24 Nov 2025 14:31:48 +0100 Subject: [PATCH 02/12] UI update --- .../vibration-anomaly-detection/assets/app.js | 78 ++++++++++--------- .../assets/index.html | 10 +-- .../assets/style.css | 19 +++++ .../python/main.py | 15 ++++ 4 files changed, 79 insertions(+), 43 deletions(-) diff --git a/examples/vibration-anomaly-detection/assets/app.js b/examples/vibration-anomaly-detection/assets/app.js index 71297e7..3f08778 100644 --- a/examples/vibration-anomaly-detection/assets/app.js +++ b/examples/vibration-anomaly-detection/assets/app.js @@ -93,7 +93,7 @@ document.addEventListener('DOMContentLoaded', () => { initSocketIO(); renderAccelerometerData(); // Initial render for accelerometer renderAnomalies(); // Initial render for anomalies - updateFeedback(false); // Initial feedback state + updateFeedback(null); // Initial feedback state initializeConfidenceSlider(); // Initialize the confidence slider // Popover logic @@ -139,11 +139,11 @@ function handleConfidenceInputChange() { const confidenceInput = document.getElementById('confidenceInput'); const confidenceSlider = document.getElementById('confidenceSlider'); - let value = parseFloat(confidenceInput.value); + let value = parseInt(confidenceInput.value, 10); - if (isNaN(value)) value = 0.5; - if (value < 0) value = 0; - if (value > 1) value = 1; + if (isNaN(value)) value = 5; + if (value < 1) value = 1; + if (value > 10) value = 10; confidenceSlider.value = value; updateConfidenceDisplay(); @@ -151,13 +151,13 @@ function handleConfidenceInputChange() { function validateConfidenceInput() { const confidenceInput = document.getElementById('confidenceInput'); - let value = parseFloat(confidenceInput.value); + let value = parseInt(confidenceInput.value, 10); - if (isNaN(value)) value = 0.5; - if (value < 0) value = 0; - if (value > 1) value = 1; + if (isNaN(value)) value = 5; + if (value < 1) value = 1; + if (value > 10) value = 10; - confidenceInput.value = value.toFixed(2); + confidenceInput.value = value.toFixed(0); handleConfidenceInputChange(); } @@ -169,10 +169,10 @@ function updateConfidenceDisplay() { const sliderProgress = document.getElementById('sliderProgress'); const value = parseFloat(confidenceSlider.value); - socket.emit('override_th', value); // Send confidence to backend + socket.emit('override_th', value / 10); // Send scaled confidence to backend (0.1 to 1.0) const percentage = (value - confidenceSlider.min) / (confidenceSlider.max - confidenceSlider.min) * 100; - const displayValue = value.toFixed(2); + const displayValue = value.toFixed(0); confidenceValueDisplay.textContent = displayValue; if (document.activeElement !== confidenceInput) { @@ -187,8 +187,8 @@ function resetConfidence() { const confidenceSlider = document.getElementById('confidenceSlider'); const confidenceInput = document.getElementById('confidenceInput'); - confidenceSlider.value = '0.5'; - confidenceInput.value = '0.50'; + confidenceSlider.value = '5'; + confidenceInput.value = '5'; updateConfidenceDisplay(); } @@ -200,7 +200,13 @@ function initSocketIO() { } printAnomalies(message); renderAnomalies(); - updateFeedback(true); + try { + const parsedAnomaly = JSON.parse(message); + updateFeedback(parsedAnomaly.score); // Pass the anomaly score + } catch (e) { + console.error("Failed to parse anomaly message for feedback:", message, e); + updateFeedback(null); // Fallback to no anomaly feedback + } }); socket.on('sample', (s) => { @@ -225,7 +231,7 @@ function initSocketIO() { // ... (existing printAnomalies and renderAnomalies functions) -function updateFeedback(hasAnomaly) { +function updateFeedback(anomalyScore = null) { clearTimeout(feedbackTimeout); // Clear any existing timeout if (!hasDataFromBackend) { @@ -238,21 +244,20 @@ function updateFeedback(hasAnomaly) { return; } - if (hasAnomaly) { + if (anomalyScore !== null) { // Anomaly detected feedbackContentWrapper.innerHTML = ` `; - // Reset to "No anomalies" state after 3 seconds feedbackTimeout = setTimeout(() => { - updateFeedback(false); + updateFeedback(null); // Reset after 3 seconds }, 3000); - } else { + } else { // No anomaly or reset feedbackContentWrapper.innerHTML = ` `; @@ -285,25 +290,22 @@ function renderAnomalies() { return; // Skip empty anomaly objects } - const row = document.createElement('div'); - row.className = 'anomaly-container'; // Using a new class for styling + const listItem = document.createElement('li'); + listItem.className = 'anomaly-list-item'; - const cellContainer = document.createElement('span'); - cellContainer.className = 'anomaly-cell-container'; + const score = parsedAnomaly.score.toFixed(1); + const date = new Date(parsedAnomaly.timestamp); - const scoreText = document.createElement('span'); - scoreText.className = 'anomaly-content'; - const value = parsedAnomaly.score; // Assuming 'score' is a property - scoreText.innerHTML = `Anomaly Score: ${value.toFixed(2)}`; + const timeString = date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const dateString = date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, ' '); - const timeText = document.createElement('span'); - timeText.className = 'anomaly-content-time'; - timeText.textContent = new Date(parsedAnomaly.timestamp).toLocaleString('it-IT').replace(',', ' -'); // Assuming 'timestamp' property + listItem.innerHTML = ` + ${score} + Anomaly + ${timeString} - ${dateString} + `; - cellContainer.appendChild(scoreText); - cellContainer.appendChild(timeText); - row.appendChild(cellContainer); - recentAnomaliesElement.appendChild(row); + recentAnomaliesElement.appendChild(listItem); } catch (e) { console.error("Failed to parse anomaly data:", anomaly, e); diff --git a/examples/vibration-anomaly-detection/assets/index.html b/examples/vibration-anomaly-detection/assets/index.html index cf2ba7f..635cc57 100644 --- a/examples/vibration-anomaly-detection/assets/index.html +++ b/examples/vibration-anomaly-detection/assets/index.html @@ -64,18 +64,18 @@

    Vibration Anomaly Detection

    Scores above your threshold are flagged as an anomaly.