Skip to content

Commit 973408e

Browse files
authored
Fan vibration monitoring UI (#19)
1 parent f8c2d75 commit 973408e

File tree

16 files changed

+974
-105
lines changed

16 files changed

+974
-105
lines changed

examples/bedtime-story-teller/assets/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ <h3 class="parameter-title">Age</h3>
4545
<span class="chip">6-8 years</span>
4646
<span class="chip">9-12 years</span>
4747
<span class="chip">13-16 years</span>
48-
<span class="chip">Adult</span>
48+
4949
</div>
5050
</div>
5151
</div>

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

Lines changed: 311 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,334 @@
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 maxSamples = 200;
8+
const samples = [];
9+
let errorContainer;
10+
11+
const recentAnomaliesElement = document.getElementById('recentClassifications');
12+
let anomalies = [];
13+
const MAX_RECENT_ANOMALIES = 5;
14+
15+
let hasDataFromBackend = false; // New global flag
16+
17+
const accelerometerDataDisplay = document.getElementById('accelerometer-data-display');
18+
const noAccelerometerDataPlaceholder = document.getElementById('no-accelerometer-data');
19+
20+
function drawPlot() {
21+
if (!hasDataFromBackend) return; // Only draw if we have data
22+
23+
const currentWidth = canvas.clientWidth;
24+
const currentHeight = canvas.clientHeight;
25+
26+
if (canvas.width !== currentWidth || canvas.height !== currentHeight) {
27+
canvas.width = currentWidth;
28+
canvas.height = currentHeight;
29+
}
30+
// Clear the canvas before drawing the new frame!
31+
ctx.clearRect(0, 0, currentWidth, currentHeight);
32+
// All grid lines (every 0.5) - same size
33+
ctx.strokeStyle = '#31333F99';
34+
ctx.lineWidth = 0.5;
35+
ctx.beginPath();
36+
for (let i=0; i<=8; i++){
37+
const y = 10 + i*((currentHeight-20)/8);
38+
ctx.moveTo(40,y);
39+
ctx.lineTo(currentWidth,y);
40+
}
41+
ctx.stroke();
42+
43+
// Y-axis labels (-2.0 to 2.0 every 0.5)
44+
ctx.fillStyle = '#666';
45+
ctx.font = '400 14px Arial';
46+
ctx.textAlign = 'right';
47+
ctx.textBaseline = 'middle';
48+
49+
for (let i=0; i<=8; i++) {
50+
const y = 10 + i*((currentHeight-20)/8);
51+
const value = (4.0 - i * 1.0).toFixed(1);
52+
ctx.fillText(value, 35, y);
53+
}
54+
55+
// draw each series
56+
function drawSeries(key, color) {
57+
ctx.strokeStyle = color;
58+
ctx.lineWidth = 1;
59+
ctx.beginPath();
60+
for (let i=0;i<samples.length;i++){
61+
const s = samples[i];
62+
const x = 40 + (i/(maxSamples-1))*(currentWidth-40);
63+
const v = s[key];
64+
const y = (currentHeight/2) - (v * ((currentHeight-20)/8));
65+
if (i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
66+
}
67+
ctx.stroke();
68+
}
69+
70+
drawSeries('x','#0068C9');
71+
drawSeries('y','#FF9900');
72+
drawSeries('z','#FF2B2B');
73+
}
74+
75+
function pushSample(s){
76+
samples.push(s);
77+
if (samples.length>maxSamples) samples.shift();
78+
if (!hasDataFromBackend) { // Check if this is the first data received
79+
hasDataFromBackend = true;
80+
renderAccelerometerData();
81+
}
82+
drawPlot();
83+
}
884

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

90+
const feedbackContentWrapper = document.getElementById('feedback-content-wrapper');
91+
let feedbackTimeout;
92+
93+
// ... (existing code between)
94+
1495
// Start the application
1596
document.addEventListener('DOMContentLoaded', () => {
1697
initSocketIO();
98+
renderAccelerometerData(); // Initial render for accelerometer
99+
renderAnomalies(); // Initial render for anomalies
100+
updateFeedback(null); // Initial feedback state
101+
initializeConfidenceSlider(); // Initialize the confidence slider
102+
103+
// Popover logic
104+
document.querySelectorAll('.info-btn.confidence').forEach(img => {
105+
const popover = img.nextElementSibling;
106+
img.addEventListener('mouseenter', () => {
107+
popover.style.display = 'block';
108+
});
109+
img.addEventListener('mouseleave', () => {
110+
popover.style.display = 'none';
111+
});
112+
});
113+
114+
document.querySelectorAll('.info-btn.accelerometer-data').forEach(img => {
115+
const popover = img.nextElementSibling;
116+
img.addEventListener('mouseenter', () => {
117+
popover.style.display = 'block';
118+
});
119+
img.addEventListener('mouseleave', () => {
120+
popover.style.display = 'none';
121+
});
122+
});
17123
});
18124

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

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

29-
changeStatus(isOn);
30-
31-
if (timeoutId) {
32-
clearTimeout(timeoutId);
238+
function updateFeedback(anomalyScore = null) {
239+
clearTimeout(feedbackTimeout); // Clear any existing timeout
240+
241+
if (!hasDataFromBackend) {
242+
feedbackContentWrapper.innerHTML = `
243+
<div class="feedback-content">
244+
<img src="./img/no-data.png" alt="No Data">
245+
<p class="feedback-text">No data</p>
246+
</div>
247+
`;
248+
return;
33249
}
34-
35-
// schedule reset
36-
timeoutId = setTimeout(() => changeStatus(!isOn), 3000);
250+
251+
if (anomalyScore !== null) { // Anomaly detected
252+
feedbackContentWrapper.innerHTML = `
253+
<div class="feedback-content">
254+
<img src="./img/bad.svg" alt="Anomaly Detected">
255+
<p class="feedback-text">Anomaly detected: ${anomalyScore.toFixed(2)}</p>
256+
</div>
257+
`;
258+
feedbackTimeout = setTimeout(() => {
259+
updateFeedback(null); // Reset after 3 seconds
260+
}, 3000);
261+
} else { // No anomaly or reset
262+
feedbackContentWrapper.innerHTML = `
263+
<div class="feedback-content">
264+
<img src="./img/good.svg" alt="No Anomalies">
265+
<p class="feedback-text">No anomalies</p>
266+
</div>
267+
`;
268+
}
269+
}
270+
271+
function printAnomalies(newAnomaly) {
272+
anomalies.unshift(newAnomaly);
273+
if (anomalies.length > MAX_RECENT_ANOMALIES) { anomalies.pop(); }
37274
}
38275

39-
function changeStatus(isOn) {
40-
fanLed.className = isOn ? 'led-on' : 'led-off';
41-
fanText.textContent = isOn ? 'Anomaly detected' : 'No anomaly';
276+
function renderAnomalies() {
277+
recentAnomaliesElement.innerHTML = ``; // Clear the list
278+
279+
if (anomalies.length === 0) {
280+
recentAnomaliesElement.innerHTML = `
281+
<div class="no-recent-anomalies">
282+
<img src="./img/no-data.png">
283+
<p>No recent anomalies</p>
284+
</div>
285+
`;
286+
return;
287+
}
288+
289+
anomalies.forEach((anomaly) => {
290+
try {
291+
const parsedAnomaly = JSON.parse(anomaly);
292+
293+
if (Object.keys(parsedAnomaly).length === 0) {
294+
return; // Skip empty anomaly objects
295+
}
296+
297+
const listItem = document.createElement('li');
298+
listItem.className = 'anomaly-list-item';
299+
300+
const score = parsedAnomaly.score.toFixed(1);
301+
const date = new Date(parsedAnomaly.timestamp);
302+
303+
const timeString = date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
304+
const dateString = date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, ' ');
305+
306+
listItem.innerHTML = `
307+
<span class="anomaly-score">${score}</span>
308+
<span class="anomaly-text">Anomaly</span>
309+
<span class="anomaly-time">${timeString} - ${dateString}</span>
310+
`;
311+
312+
recentAnomaliesElement.appendChild(listItem);
313+
314+
} catch (e) {
315+
console.error("Failed to parse anomaly data:", anomaly, e);
316+
if(recentAnomaliesElement.getElementsByClassName('anomaly-error').length === 0) {
317+
const errorRow = document.createElement('div');
318+
errorRow.className = 'anomaly-error';
319+
errorRow.textContent = `Error processing anomaly data. Check console for details.`;
320+
recentAnomaliesElement.appendChild(errorRow);
321+
}
322+
}
323+
});
324+
}
325+
326+
function renderAccelerometerData() {
327+
if (hasDataFromBackend) {
328+
accelerometerDataDisplay.style.display = 'block';
329+
noAccelerometerDataPlaceholder.style.display = 'none';
330+
drawPlot();
331+
} else {
332+
accelerometerDataDisplay.style.display = 'none';
333+
noAccelerometerDataPlaceholder.style.display = 'flex'; // Use flex for centering content
334+
}
42335
}

0 commit comments

Comments
 (0)