Skip to content

Commit 610e9d6

Browse files
committed
Init fan vibration monitoring
1 parent 59b568f commit 610e9d6

File tree

12 files changed

+906
-78
lines changed

12 files changed

+906
-78
lines changed

examples/vibration-anomaly-detection/assets/app.js

Lines changed: 305 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,328 @@
22
//
33
// SPDX-License-Identifier: MPL-2.0
44

5-
const fanLed = document.getElementById('fan-led');
6-
const fanText = document.getElementById('fan-text');
7-
let timeoutId;
5+
const canvas = document.getElementById('plot');
6+
const ctx = canvas.getContext('2d');
7+
const width = canvas.width, height = canvas.height;
8+
const maxSamples = 200;
9+
const samples = [];
10+
let errorContainer;
11+
12+
const recentAnomaliesElement = document.getElementById('recentClassifications'); // Renamed to recentAnomaliesElement but ID is recentClassifications
13+
let anomalies = [];
14+
const MAX_RECENT_ANOMALIES = 5;
15+
16+
let hasDataFromBackend = false; // New global flag
17+
18+
const accelerometerDataDisplay = document.getElementById('accelerometer-data-display');
19+
const noAccelerometerDataPlaceholder = document.getElementById('no-accelerometer-data');
20+
21+
function drawPlot() {
22+
if (!hasDataFromBackend) return; // Only draw if we have data
23+
24+
// clear
25+
// ctx.fillStyle = '#fff';
26+
// ctx.fillRect(0,0,width,height);
27+
28+
// All grid lines (every 0.5) - same size
29+
ctx.strokeStyle = 'rgba(49, 51, 63, 0.6)';
30+
ctx.lineWidth = 0.5;
31+
ctx.beginPath();
32+
for (let i=0; i<=8; i++){
33+
const y = 10 + i*((height-20)/8);
34+
ctx.moveTo(40,y);
35+
ctx.lineTo(width,y);
36+
}
37+
ctx.stroke();
38+
39+
// Y-axis labels (-2.0 to 2.0 every 0.5)
40+
ctx.fillStyle = 'rgba(49, 51, 63, 0.6)';
41+
ctx.font = '12px Arial';
42+
ctx.textAlign = 'right';
43+
ctx.textBaseline = 'middle';
44+
45+
for (let i=0; i<=8; i++) {
46+
const y = 10 + i*((height-20)/8);
47+
const value = (2.0 - i * 0.5).toFixed(1);
48+
ctx.fillText(value, 35, y);
49+
}
50+
51+
// draw each series
52+
function drawSeries(key, color) {
53+
ctx.strokeStyle = color;
54+
ctx.lineWidth = 1;
55+
ctx.beginPath();
56+
for (let i=0;i<samples.length;i++){
57+
const s = samples[i];
58+
const x = 40 + (i/(maxSamples-1))*(width-40);
59+
const v = s[key];
60+
const y = (height/2) - (v * ((height-20)/4));
61+
if (i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
62+
}
63+
ctx.stroke();
64+
}
65+
66+
drawSeries('x','#0068C9');
67+
drawSeries('y','#FF9900');
68+
drawSeries('z','#FF2B2B');
69+
}
70+
71+
function pushSample(s){
72+
samples.push(s);
73+
if (samples.length>maxSamples) samples.shift();
74+
if (!hasDataFromBackend) { // Check if this is the first data received
75+
hasDataFromBackend = true;
76+
renderAccelerometerData();
77+
}
78+
drawPlot();
79+
}
880

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

86+
const feedbackContentWrapper = document.getElementById('feedback-content-wrapper');
87+
let feedbackTimeout;
88+
89+
// ... (existing code between)
90+
1491
// Start the application
1592
document.addEventListener('DOMContentLoaded', () => {
1693
initSocketIO();
94+
renderAccelerometerData(); // Initial render for accelerometer
95+
renderAnomalies(); // Initial render for anomalies
96+
updateFeedback(false); // Initial feedback state
97+
initializeConfidenceSlider(); // Initialize the confidence slider
98+
99+
// Popover logic
100+
document.querySelectorAll('.info-btn.confidence').forEach(img => {
101+
const popover = img.nextElementSibling;
102+
img.addEventListener('mouseenter', () => {
103+
popover.style.display = 'block';
104+
});
105+
img.addEventListener('mouseleave', () => {
106+
popover.style.display = 'none';
107+
});
108+
});
109+
110+
document.querySelectorAll('.info-btn.accelerometer-data').forEach(img => {
111+
const popover = img.nextElementSibling;
112+
img.addEventListener('mouseenter', () => {
113+
popover.style.display = 'block';
114+
});
115+
img.addEventListener('mouseleave', () => {
116+
popover.style.display = 'none';
117+
});
118+
});
17119
});
18120

121+
function initializeConfidenceSlider() {
122+
const confidenceSlider = document.getElementById('confidenceSlider');
123+
const confidenceInput = document.getElementById('confidenceInput');
124+
const confidenceResetButton = document.getElementById('confidenceResetButton');
125+
126+
confidenceSlider.addEventListener('input', updateConfidenceDisplay);
127+
confidenceInput.addEventListener('input', handleConfidenceInputChange);
128+
confidenceInput.addEventListener('blur', validateConfidenceInput);
129+
updateConfidenceDisplay();
130+
131+
confidenceResetButton.addEventListener('click', (e) => {
132+
if (e.target.classList.contains('reset-icon') || e.target.closest('.reset-icon')) {
133+
resetConfidence();
134+
}
135+
});
136+
}
137+
138+
function handleConfidenceInputChange() {
139+
const confidenceInput = document.getElementById('confidenceInput');
140+
const confidenceSlider = document.getElementById('confidenceSlider');
141+
142+
let value = parseFloat(confidenceInput.value);
143+
144+
if (isNaN(value)) value = 0.5;
145+
if (value < 0) value = 0;
146+
if (value > 1) value = 1;
147+
148+
confidenceSlider.value = value;
149+
updateConfidenceDisplay();
150+
}
151+
152+
function validateConfidenceInput() {
153+
const confidenceInput = document.getElementById('confidenceInput');
154+
let value = parseFloat(confidenceInput.value);
155+
156+
if (isNaN(value)) value = 0.5;
157+
if (value < 0) value = 0;
158+
if (value > 1) value = 1;
159+
160+
confidenceInput.value = value.toFixed(2);
161+
162+
handleConfidenceInputChange();
163+
}
164+
165+
function updateConfidenceDisplay() {
166+
const confidenceSlider = document.getElementById('confidenceSlider');
167+
const confidenceInput = document.getElementById('confidenceInput');
168+
const confidenceValueDisplay = document.getElementById('confidenceValueDisplay');
169+
const sliderProgress = document.getElementById('sliderProgress');
170+
171+
const value = parseFloat(confidenceSlider.value);
172+
socket.emit('override_th', value); // Send confidence to backend
173+
const percentage = (value - confidenceSlider.min) / (confidenceSlider.max - confidenceSlider.min) * 100;
174+
175+
const displayValue = value.toFixed(2);
176+
confidenceValueDisplay.textContent = displayValue;
177+
178+
if (document.activeElement !== confidenceInput) {
179+
confidenceInput.value = displayValue;
180+
}
181+
182+
sliderProgress.style.width = percentage + '%';
183+
confidenceValueDisplay.style.left = percentage + '%';
184+
}
185+
186+
function resetConfidence() {
187+
const confidenceSlider = document.getElementById('confidenceSlider');
188+
const confidenceInput = document.getElementById('confidenceInput');
189+
190+
confidenceSlider.value = '0.5';
191+
confidenceInput.value = '0.50';
192+
updateConfidenceDisplay();
193+
}
194+
19195
function initSocketIO() {
20-
socket.on('fan_status_update', (message) => {
21-
updateFanStatus(message);
196+
socket.on('anomaly_detected', async (message) => {
197+
if (!hasDataFromBackend) { // Check if this is the first data received
198+
hasDataFromBackend = true;
199+
renderAccelerometerData();
200+
}
201+
printAnomalies(message);
202+
renderAnomalies();
203+
updateFeedback(true);
204+
});
205+
206+
socket.on('sample', (s) => {
207+
pushSample(s);
208+
});
209+
210+
socket.on('connect', () => {
211+
if (errorContainer) {
212+
errorContainer.style.display = 'none';
213+
errorContainer.textContent = '';
214+
}
215+
});
216+
217+
socket.on('disconnect', () => {
218+
errorContainer = document.getElementById('error-container');
219+
if (errorContainer) {
220+
errorContainer.textContent = 'Connection to the board lost. Please check the connection.';
221+
errorContainer.style.display = 'block';
222+
}
22223
});
23224
}
24225

25-
// Function to update LED status in the UI
26-
function updateFanStatus(status) {
27-
const isOn = status.anomaly;
226+
// ... (existing printAnomalies and renderAnomalies functions)
28227

29-
changeStatus(isOn);
30-
31-
if (timeoutId) {
32-
clearTimeout(timeoutId);
228+
function updateFeedback(hasAnomaly) {
229+
clearTimeout(feedbackTimeout); // Clear any existing timeout
230+
231+
if (!hasDataFromBackend) {
232+
feedbackContentWrapper.innerHTML = `
233+
<div class="feedback-content">
234+
<img src="./img/no-data.png" alt="No Data">
235+
<p class="feedback-text">No data</p>
236+
</div>
237+
`;
238+
return;
33239
}
34-
35-
// schedule reset
36-
timeoutId = setTimeout(() => changeStatus(!isOn), 3000);
240+
241+
if (hasAnomaly) {
242+
feedbackContentWrapper.innerHTML = `
243+
<div class="feedback-content">
244+
<img src="./img/good.svg" alt="Anomaly Detected">
245+
<p class="feedback-text">Anomaly Detected!</p>
246+
</div>
247+
`;
248+
// Reset to "No anomalies" state after 3 seconds
249+
feedbackTimeout = setTimeout(() => {
250+
updateFeedback(false);
251+
}, 3000);
252+
} else {
253+
feedbackContentWrapper.innerHTML = `
254+
<div class="feedback-content">
255+
<img src="./img/bad.svg" alt="No Anomalies">
256+
<p class="feedback-text">No anomalies</p>
257+
</div>
258+
`;
259+
}
260+
}
261+
262+
function printAnomalies(newAnomaly) {
263+
anomalies.unshift(newAnomaly);
264+
if (anomalies.length > MAX_RECENT_ANOMALIES) { anomalies.pop(); }
37265
}
38266

39-
function changeStatus(isOn) {
40-
fanLed.className = isOn ? 'led-on' : 'led-off';
41-
fanText.textContent = isOn ? 'Anomaly detected' : 'No anomaly';
267+
function renderAnomalies() {
268+
recentAnomaliesElement.innerHTML = ``; // Clear the list
269+
270+
if (anomalies.length === 0) {
271+
recentAnomaliesElement.innerHTML = `
272+
<div class="no-recent-anomalies">
273+
<img src="./img/no-data.png">
274+
<p>No recent anomalies</p>
275+
</div>
276+
`;
277+
return;
278+
}
279+
280+
anomalies.forEach((anomaly) => {
281+
try {
282+
const parsedAnomaly = JSON.parse(anomaly);
283+
284+
if (Object.keys(parsedAnomaly).length === 0) {
285+
return; // Skip empty anomaly objects
286+
}
287+
288+
const row = document.createElement('div');
289+
row.className = 'anomaly-container'; // Using a new class for styling
290+
291+
const cellContainer = document.createElement('span');
292+
cellContainer.className = 'anomaly-cell-container';
293+
294+
const scoreText = document.createElement('span');
295+
scoreText.className = 'anomaly-content';
296+
const value = parsedAnomaly.score; // Assuming 'score' is a property
297+
scoreText.innerHTML = `Anomaly Score: ${value.toFixed(2)}`;
298+
299+
const timeText = document.createElement('span');
300+
timeText.className = 'anomaly-content-time';
301+
timeText.textContent = new Date(parsedAnomaly.timestamp).toLocaleString('it-IT').replace(',', ' -'); // Assuming 'timestamp' property
302+
303+
cellContainer.appendChild(scoreText);
304+
cellContainer.appendChild(timeText);
305+
row.appendChild(cellContainer);
306+
recentAnomaliesElement.appendChild(row);
307+
308+
} catch (e) {
309+
console.error("Failed to parse anomaly data:", anomaly, e);
310+
if(recentAnomaliesElement.getElementsByClassName('anomaly-error').length === 0) {
311+
const errorRow = document.createElement('div');
312+
errorRow.className = 'anomaly-error';
313+
errorRow.textContent = `Error processing anomaly data. Check console for details.`;
314+
recentAnomaliesElement.appendChild(errorRow);
315+
}
316+
}
317+
});
318+
}
319+
320+
function renderAccelerometerData() {
321+
if (hasDataFromBackend) {
322+
accelerometerDataDisplay.style.display = 'block';
323+
noAccelerometerDataPlaceholder.style.display = 'none';
324+
drawPlot();
325+
} else {
326+
accelerometerDataDisplay.style.display = 'none';
327+
noAccelerometerDataPlaceholder.style.display = 'flex'; // Use flex for centering content
328+
}
42329
}

0 commit comments

Comments
 (0)