Skip to content
Open
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
328 changes: 310 additions & 18 deletions examples/vibration-anomaly-detection/assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,333 @@
//
// SPDX-License-Identifier: MPL-2.0

const fanLed = document.getElementById('fan-led');
const fanText = document.getElementById('fan-text');
let timeoutId;
const canvas = document.getElementById('plot');
const ctx = canvas.getContext('2d');
const maxSamples = 200;
const samples = [];
let errorContainer;

const recentAnomaliesElement = document.getElementById('recentClassifications');
let anomalies = [];
const MAX_RECENT_ANOMALIES = 5;

let hasDataFromBackend = false; // New global flag

const accelerometerDataDisplay = document.getElementById('accelerometer-data-display');
const noAccelerometerDataPlaceholder = document.getElementById('no-accelerometer-data');

function drawPlot() {
if (!hasDataFromBackend) return; // Only draw if we have data

const currentWidth = canvas.clientWidth;
const currentHeight = canvas.clientHeight;

if (canvas.width !== currentWidth || canvas.height !== currentHeight) {
canvas.width = currentWidth;
canvas.height = currentHeight;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Clear the canvas before drawing the new frame!
ctx.clearRect(0, 0, currentWidth, currentHeight);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested the code (I know this is work in progress) but I noticed that the chart was not being cleared and was showing an overlapped messy line chart. With this addition, this is fixed 🙏

// All grid lines (every 0.5) - same size
ctx.strokeStyle = '#31333F99';
ctx.lineWidth = 0.5;
ctx.beginPath();
for (let i=0; i<=8; i++){
const y = 10 + i*((currentHeight-20)/8);
ctx.moveTo(40,y);
ctx.lineTo(currentWidth,y);
}
ctx.stroke();

// Y-axis labels (-2.0 to 2.0 every 0.5)
ctx.fillStyle = '#666';
ctx.font = '400 14px Arial';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';

for (let i=0; i<=8; i++) {
const y = 10 + i*((currentHeight-20)/8);
const value = (2.0 - i * 0.5).toFixed(1);
ctx.fillText(value, 35, y);
}

// draw each series
function drawSeries(key, color) {
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.beginPath();
for (let i=0;i<samples.length;i++){
const s = samples[i];
const x = 40 + (i/(maxSamples-1))*(currentWidth-40);
const v = s[key];
const y = (currentHeight/2) - (v * ((currentHeight-20)/4));
if (i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
}
ctx.stroke();
}

drawSeries('x','#0068C9');
drawSeries('y','#FF9900');
drawSeries('z','#FF2B2B');
}

function pushSample(s){
samples.push(s);
if (samples.length>maxSamples) samples.shift();
if (!hasDataFromBackend) { // Check if this is the first data received
hasDataFromBackend = true;
renderAccelerometerData();
}
drawPlot();
}

/*
* Socket initialization. We need it to communicate with the server
*/
const socket = io(`http://${window.location.host}`); // Initialize socket.io connection

const feedbackContentWrapper = document.getElementById('feedback-content-wrapper');
let feedbackTimeout;

// ... (existing code between)

// Start the application
document.addEventListener('DOMContentLoaded', () => {
initSocketIO();
renderAccelerometerData(); // Initial render for accelerometer
renderAnomalies(); // Initial render for anomalies
updateFeedback(null); // Initial feedback state
initializeConfidenceSlider(); // Initialize the confidence slider

// Popover logic
document.querySelectorAll('.info-btn.confidence').forEach(img => {
const popover = img.nextElementSibling;
img.addEventListener('mouseenter', () => {
popover.style.display = 'block';
});
img.addEventListener('mouseleave', () => {
popover.style.display = 'none';
});
});

document.querySelectorAll('.info-btn.accelerometer-data').forEach(img => {
const popover = img.nextElementSibling;
img.addEventListener('mouseenter', () => {
popover.style.display = 'block';
});
img.addEventListener('mouseleave', () => {
popover.style.display = 'none';
});
});
});

function initializeConfidenceSlider() {
const confidenceSlider = document.getElementById('confidenceSlider');
const confidenceInput = document.getElementById('confidenceInput');
const confidenceResetButton = document.getElementById('confidenceResetButton');

confidenceSlider.addEventListener('input', updateConfidenceDisplay);
confidenceInput.addEventListener('input', handleConfidenceInputChange);
confidenceInput.addEventListener('blur', validateConfidenceInput);
updateConfidenceDisplay();

confidenceResetButton.addEventListener('click', (e) => {
if (e.target.classList.contains('reset-icon') || e.target.closest('.reset-icon')) {
resetConfidence();
}
});
}

function handleConfidenceInputChange() {
const confidenceInput = document.getElementById('confidenceInput');
const confidenceSlider = document.getElementById('confidenceSlider');

let value = parseInt(confidenceInput.value, 10);

if (isNaN(value)) value = 5;
if (value < 1) value = 1;
if (value > 10) value = 10;

confidenceSlider.value = value;
updateConfidenceDisplay();
}

function validateConfidenceInput() {
const confidenceInput = document.getElementById('confidenceInput');
let value = parseInt(confidenceInput.value, 10);

if (isNaN(value)) value = 5;
if (value < 1) value = 1;
if (value > 10) value = 10;

confidenceInput.value = value.toFixed(0);

handleConfidenceInputChange();
}

function updateConfidenceDisplay() {
const confidenceSlider = document.getElementById('confidenceSlider');
const confidenceInput = document.getElementById('confidenceInput');
const confidenceValueDisplay = document.getElementById('confidenceValueDisplay');
const sliderProgress = document.getElementById('sliderProgress');

const value = parseFloat(confidenceSlider.value);
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(0);
confidenceValueDisplay.textContent = displayValue;

if (document.activeElement !== confidenceInput) {
confidenceInput.value = displayValue;
}

sliderProgress.style.width = percentage + '%';
confidenceValueDisplay.style.left = percentage + '%';
}

function resetConfidence() {
const confidenceSlider = document.getElementById('confidenceSlider');
const confidenceInput = document.getElementById('confidenceInput');

confidenceSlider.value = '5';
confidenceInput.value = '5';
updateConfidenceDisplay();
}

function initSocketIO() {
socket.on('fan_status_update', (message) => {
updateFanStatus(message);
socket.on('anomaly_detected', async (message) => {
if (!hasDataFromBackend) { // Check if this is the first data received
hasDataFromBackend = true;
renderAccelerometerData();
}
printAnomalies(message);
renderAnomalies();
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) => {
pushSample(s);
});

socket.on('connect', () => {
if (errorContainer) {
errorContainer.style.display = 'none';
errorContainer.textContent = '';
}
});

socket.on('disconnect', () => {
errorContainer = document.getElementById('error-container');
if (errorContainer) {
errorContainer.textContent = 'Connection to the board lost. Please check the connection.';
errorContainer.style.display = 'block';
}
});
}

// Function to update LED status in the UI
function updateFanStatus(status) {
const isOn = status.anomaly;
// ... (existing printAnomalies and renderAnomalies functions)

changeStatus(isOn);

if (timeoutId) {
clearTimeout(timeoutId);
function updateFeedback(anomalyScore = null) {
clearTimeout(feedbackTimeout); // Clear any existing timeout

if (!hasDataFromBackend) {
feedbackContentWrapper.innerHTML = `
<div class="feedback-content">
<img src="./img/no-data.png" alt="No Data">
<p class="feedback-text">No data</p>
</div>
`;
return;
}

// schedule reset
timeoutId = setTimeout(() => changeStatus(!isOn), 3000);

if (anomalyScore !== null) { // Anomaly detected
feedbackContentWrapper.innerHTML = `
<div class="feedback-content">
<img src="./img/bad.svg" alt="Anomaly Detected">
<p class="feedback-text">Anomaly detected: ${anomalyScore.toFixed(2)}</p>
</div>
`;
feedbackTimeout = setTimeout(() => {
updateFeedback(null); // Reset after 3 seconds
}, 3000);
} else { // No anomaly or reset
feedbackContentWrapper.innerHTML = `
<div class="feedback-content">
<img src="./img/good.svg" alt="No Anomalies">
<p class="feedback-text">No anomalies</p>
</div>
`;
}
}

function printAnomalies(newAnomaly) {
anomalies.unshift(newAnomaly);
if (anomalies.length > MAX_RECENT_ANOMALIES) { anomalies.pop(); }
}

function changeStatus(isOn) {
fanLed.className = isOn ? 'led-on' : 'led-off';
fanText.textContent = isOn ? 'Anomaly detected' : 'No anomaly';
function renderAnomalies() {
recentAnomaliesElement.innerHTML = ``; // Clear the list

if (anomalies.length === 0) {
recentAnomaliesElement.innerHTML = `
<div class="no-recent-anomalies">
<img src="./img/no-data.png">
<p>No recent anomalies</p>
</div>
`;
return;
}

anomalies.forEach((anomaly) => {
try {
const parsedAnomaly = JSON.parse(anomaly);

if (Object.keys(parsedAnomaly).length === 0) {
return; // Skip empty anomaly objects
}

const listItem = document.createElement('li');
listItem.className = 'anomaly-list-item';

const score = parsedAnomaly.score.toFixed(1);
const date = new Date(parsedAnomaly.timestamp);

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, ' ');

listItem.innerHTML = `
<span class="anomaly-score">${score}</span>
<span class="anomaly-text">Anomaly</span>
<span class="anomaly-time">${timeString} - ${dateString}</span>
`;

recentAnomaliesElement.appendChild(listItem);

} catch (e) {
console.error("Failed to parse anomaly data:", anomaly, e);
if(recentAnomaliesElement.getElementsByClassName('anomaly-error').length === 0) {
const errorRow = document.createElement('div');
errorRow.className = 'anomaly-error';
errorRow.textContent = `Error processing anomaly data. Check console for details.`;
recentAnomaliesElement.appendChild(errorRow);
}
}
});
}

function renderAccelerometerData() {
if (hasDataFromBackend) {
accelerometerDataDisplay.style.display = 'block';
noAccelerometerDataPlaceholder.style.display = 'none';
drawPlot();
} else {
accelerometerDataDisplay.style.display = 'none';
noAccelerometerDataPlaceholder.style.display = 'flex'; // Use flex for centering content
}
}
Loading