diff --git a/Web/index.html b/Web/index.html index 1b5ebff..215a498 100644 --- a/Web/index.html +++ b/Web/index.html @@ -5,27 +5,84 @@ Tempro - Live Temperature Monitoring - + + + - -
-
-

🌡️ Live Temperature Monitor

- MQTT: Connecting... + + +
+
+

🌡️ Live Temperature Monitor

+ MQTT: Connecting... +
+ +
+ Status: Normal +
+ +
+
+
+ +
+ +
+ +
+

📅 Sensor Last Seen

+
+ + + + + + + + + + +
SensorTempSeen
+
+
+
+ +
+
+

🗂️ Temperature Logs

+ +
+
-
Status: Normal
-
- +
+

📊 Analytics

+
+ +
-
- + \ No newline at end of file diff --git a/Web/script.js b/Web/script.js index 1deec0d..40528ee 100644 --- a/Web/script.js +++ b/Web/script.js @@ -1,13 +1,50 @@ -const client = mqtt.connect('ws://serverip:9001', { - username: 'user', - password: 'password' +// SweetAlert Error Alert +function showErrorAlert(message) { + Swal.fire({ + background: '#1e293b', + icon: 'error', + title: 'Error', + text: message, + timer: 5000, + showConfirmButton: false, + timerProgressBar: true + }); +} + +// Firebase Configuration +const firebaseConfig = { + apiKey: "AIzaSyDeNFTMYiKVVtrnTQHZFb_-Uhh307CrEaU", + authDomain: "temprocw.firebaseapp.com", + projectId: "temprocw", + storageBucket: "temprocw.appspot.com", + messagingSenderId: "537290462610", + appId: "1:537290462610:web:0b653f682f4a177e9f1d4b" +}; + +try { + if (!firebase.apps.length) firebase.initializeApp(firebaseConfig); +} catch (error) { + console.error('Firebase init error:', error); + showErrorAlert('Failed to initialize Firebase.'); +} + +const db = firebase.firestore(); +const client = mqtt.connect('ws://dev.streakon.net:9001', { + username: 'tempro', + password: 'firstfloor' }); +const mqttStatus = document.getElementById('mqttStatus'); +const statusText = document.getElementById('statusText'); +const ctx = document.getElementById('tempChart').getContext('2d'); + const sensorDataMap = new Map(); const sensorLastSeenMap = new Map(); -const sensorOffsetMap = new Map(); // Calibrated Offset Map -const ctx = document.getElementById('tempChart').getContext('2d'); -const mqttStatus = document.getElementById('mqttStatus'); +const sensorOffsetMap = new Map(); +let calibrationBaseline = null; + +window.sensorDataMap = sensorDataMap; +window.sensorOffsetMap = sensorOffsetMap; const chart = new Chart(ctx, { type: 'line', @@ -16,14 +53,9 @@ const chart = new Chart(ctx, { datasets: [ { label: 'Temperature (°C)', - hidden: false, data: [], - borderColor: (context) => { - const value = context.raw; - if (value > 30) return 'red'; // High danger - if (value > 25) return 'orange'; // Warm - return 'hsl(189, 62.40%, 38.60%)'; // Safe (green) - }, + hidden: false, + borderColor: ({ raw }) => raw > 30 ? 'red' : raw > 25 ? 'orange' : 'hsl(189, 62.40%, 38.60%)', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true, tension: 0.4, @@ -31,16 +63,11 @@ const chart = new Chart(ctx, { pointHoverRadius: 8, pointBorderWidth: 2, pointBorderColor: '#ffffff', - pointBackgroundColor: (context) => { - const value = context.raw; - if (value > 30) return 'red'; - if (value > 25) return 'orange'; - return '#4caf50'; - }, + pointBackgroundColor: ({ raw }) => raw > 30 ? 'red' : raw > 25 ? 'orange' : '#4caf50', }, { label: 'Calibration Baseline', - data: [], // Will be filled dynamically + data: [], borderColor: '#1d8cf8', borderDash: [5, 5], borderWidth: 2, @@ -49,14 +76,9 @@ const chart = new Chart(ctx, { }, { label: 'Calibrated Temperature (°C)', - hidden: true, data: [], - borderColor: (context) => { - const value = context.raw; - if (value > 30) return 'red'; // High danger - if (value > 25) return 'orange'; // Warm - return 'hsl(115, 62.40%, 38.60%)'; // Safe (green) - }, + hidden: true, + borderColor: ({ raw }) => raw > 30 ? 'red' : raw > 25 ? 'orange' : 'hsl(115, 62.40%, 38.60%)', backgroundColor: 'rgba(59, 246, 246, 0.1)', fill: true, tension: 0.4, @@ -64,12 +86,7 @@ const chart = new Chart(ctx, { pointHoverRadius: 8, pointBorderWidth: 2, pointBorderColor: '#ffffff', - pointBackgroundColor: (context) => { - const value = context.raw; - if (value > 30) return 'red'; - if (value > 25) return 'orange'; - return '#4caf50'; - }, + pointBackgroundColor: ({ raw }) => raw > 30 ? 'red' : raw > 25 ? 'orange' : '#4caf50', } ] }, @@ -78,33 +95,77 @@ const chart = new Chart(ctx, { maintainAspectRatio: false, scales: { y: { - beginAtZero: false, - title: { - display: true, - text: 'Temperature (°C)', - color: '#ffffff' - }, + title: { display: true, text: 'Temperature (°C)', color: '#ffffff' }, ticks: { color: '#ffffff' }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, x: { - title: { - display: true, - text: 'Sensor ID', - color: '#ffffff' - }, + title: { display: true, text: 'Sensor ID', color: '#ffffff' }, ticks: { color: '#ffffff' }, grid: { color: 'rgba(255, 255, 255, 0.05)' } } }, plugins: { - legend: { - labels: { color: '#ffffff' } + legend: { labels: { color: '#ffffff' } } + } + } +}); + +const analyticsCtx = document.getElementById('analyticsChart').getContext('2d'); +const sensorColors = ['#4caf50', '#ff9800', '#2196f3', '#f44336', '#9c27b0', '#3f51b5', '#009688', '#795548', '#607d8b', '#00bcd4']; + +const analyticsChart = new Chart(analyticsCtx, { + type: 'line', + data: { + labels: [], + datasets: [] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + title: { display: true, text: 'Temperature (°C)', color: '#ffffff' }, + ticks: { color: '#ffffff' }, + grid: { color: 'rgba(255, 255, 255, 0.1)' } + }, + x: { + title: { display: true, text: 'Time', color: '#ffffff' }, + ticks: { color: '#ffffff' }, + grid: { color: 'rgba(255, 255, 255, 0.05)' } } + }, + plugins: { + legend: { labels: { color: '#ffffff' } } } } }); +// Last Seen Table Update +function updateLastSeenTable() { + const tbody = document.getElementById('lastSeenTableBody'); + if (!tbody) return; + + tbody.innerHTML = ''; + + const entries = [...sensorDataMap.entries()].sort((a, b) => + parseInt(a[0].replace('sensor', '')) - parseInt(b[0].replace('sensor', '')) + ); + + for (const [sensor, temp] of entries) { + const lastSeen = sensorLastSeenMap.get(sensor); + const lastSeenTime = lastSeen ? new Date(lastSeen).toLocaleTimeString() : 'N/A'; + + const row = ` + + ${sensor} + ${(temp+(sensorOffsetMap.get(sensor) || 0)).toFixed(2)} + ${lastSeenTime} + + `; + tbody.insertAdjacentHTML('beforeend', row); + } +} function updateChart() { const now = Date.now(); @@ -116,150 +177,283 @@ function updateChart() { } } - const entries = Array.from(sensorDataMap.entries()).sort((a, b) => { - const aNum = parseInt(a[0].replace('sensor', '')); - const bNum = parseInt(b[0].replace('sensor', '')); - return aNum - bNum; - }); + const entries = [...sensorDataMap.entries()].sort((a, b) => + parseInt(a[0].replace('sensor', '')) - parseInt(b[0].replace('sensor', '')) + ); const labels = entries.map(([sensor]) => sensor); - const values = entries.map(([sensor, temp]) => temp); - const calibratedValues = entries.map(([sensor, temp]) => temp + (sensorOffsetMap.get(sensor) || 0)); + const values = entries.map(([_, temp]) => temp); + const calibrated = entries.map(([sensor, temp]) => temp + (sensorOffsetMap.get(sensor) || 0)); chart.data.labels = labels; chart.data.datasets[0].data = values; - chart.data.datasets[2].data = calibratedValues; - if (values.length > 0) { - const minTemp = Math.min(...values, ...calibratedValues); - const maxTemp = Math.max(...values, ...calibratedValues); - chart.options.scales.y.min = Math.floor(minTemp - 5); - chart.options.scales.y.max = Math.ceil(maxTemp + 5); - - const highestTemp = Math.max(...calibratedValues); - - if (highestTemp > 30) { - statusText.textContent = 'High'; - statusText.className = 'text-red-500 font-bold'; - } else if (highestTemp > 25) { - statusText.textContent = 'Medium'; - statusText.className = 'text-orange-500 font-bold'; - } else { - statusText.textContent = 'Normal'; - statusText.className = 'text-green-500 font-bold'; - } + chart.data.datasets[2].data = calibrated; + + if (values.length) { + const min = Math.min(...values, ...calibrated); + const max = Math.max(...values, ...calibrated); + chart.options.scales.y.min = Math.floor(min - 5); + chart.options.scales.y.max = Math.ceil(max + 5); + + const maxCalibrated = Math.max(...calibrated); + statusText.textContent = maxCalibrated > 30 ? 'High' : maxCalibrated > 25 ? 'Medium' : 'Normal'; + statusText.className = maxCalibrated > 30 + ? 'text-red-500 font-bold' + : maxCalibrated > 25 + ? 'text-orange-500 font-bold' + : 'text-green-500 font-bold'; if (calibrationBaseline !== null) { - chart.data.datasets[1].data = chart.data.labels.map(() => calibrationBaseline); + chart.data.datasets[1].data = labels.map(() => calibrationBaseline); } } chart.update(); +} +async function updateTemperatureLogs() { + if (sensorDataMap.size === 0) return; // No data to save - chart.update(); + // Use current ISO timestamp as doc ID (second precision) + const now = new Date(); + // Cut off milliseconds to reduce duplicates — ISO string to seconds only + const timestampId = now.toISOString().split('.')[0] + 'Z'; // e.g. "2025-06-06T11:30:00Z" + + const docRef = db.collection('temperatureLogs').doc(timestampId); + + try { + const doc = await docRef.get(); + if (doc.exists) { + console.log(`Temperature log for ${timestampId} already exists, skipping write.`); + return; // Or you could update if you want + } + + // Prepare readings object from sensorDataMap + const readings = {}; + sensorDataMap.forEach((temp, sensor) => { + readings[sensor] = temp + (sensorOffsetMap.get(sensor) || 0); + }); + await docRef.set({ readings, timestamp: now.toISOString() }); + + console.log(`Temperature log saved at ${timestampId}`); + + } catch (error) { + console.error('Error saving temperature log:', error); + showErrorAlert('Failed to save temperature log.'); + } } -let calibrationBaseline = null; + +// Firestore Load/Save +function loadOffsetsFromFirestore() { + db.collection('sensorOffsets').onSnapshot(snapshot => { + sensorOffsetMap.clear(); + let hasOffsets = false; + + snapshot.forEach(doc => { + if (doc.id === 'calibrationBaseline') { + calibrationBaseline = doc.data().baseline; + } else { + sensorOffsetMap.set(doc.id, doc.data().offset); + hasOffsets = true; + } + }); + + chart.getDatasetMeta(0).hidden = hasOffsets; + chart.getDatasetMeta(2).hidden = !hasOffsets; + updateChart(); + }, error => { + console.error('Firestore error:', error); + showErrorAlert('Failed to load sensor offsets.'); + }); +} + +document.getElementById('loadLogsBtn').addEventListener('click', async () => { + const logsContainer = document.getElementById('logsContainer'); + const logsTableBody = document.getElementById('logsTableBody'); + + if (!logsContainer.classList.contains('hidden')) { + logsContainer.classList.add('hidden'); + return; + } + + logsTableBody.innerHTML = 'Loading...'; + logsContainer.classList.remove('hidden'); + + try { + const snapshot = await db.collection('temperatureLogs') + .orderBy('timestamp', 'asc') + .limit(100) + .get(); + + if (snapshot.empty) { + logsTableBody.innerHTML = 'No logs found.'; + return; + } + + logsTableBody.innerHTML = ''; + + const sensorSeries = {}; + const timestamps = []; + + snapshot.forEach(doc => { + const data = doc.data(); + const timestamp = new Date(data.timestamp).toLocaleString(); + const timeLabel = new Date(data.timestamp).toLocaleTimeString(); + timestamps.push(timeLabel); + + const readings = data.readings || {}; + const readingsText = Object.entries(readings) + .map(([sensor, temp]) => `${sensor}: ${temp.toFixed(2)}°C`) + .join(', '); + + const row = ` + + ${timestamp} + ${readingsText} + + `; + logsTableBody.insertAdjacentHTML('beforeend', row); + + for (const [sensor, temp] of Object.entries(readings)) { + if (!sensorSeries[sensor]) sensorSeries[sensor] = []; + sensorSeries[sensor].push(temp); + } + }); + + const datasets = Object.entries(sensorSeries).map(([sensor, temps], i) => ({ + label: sensor, + data: temps, + borderColor: sensorColors[i % sensorColors.length], + backgroundColor: 'transparent', + tension: 0.4, + fill: false, + pointRadius: 3, + pointHoverRadius: 6 + })); + + analyticsChart.data.labels = timestamps; + analyticsChart.data.datasets = datasets; + analyticsChart.update(); + + } catch (error) { + logsTableBody.innerHTML = `Error loading logs.`; + console.error('Error loading temperature logs:', error); + showErrorAlert('Could not load logs.'); + } +}); + + +function saveOffsetsToFirestore() { + const updates = [...sensorOffsetMap.entries()].map(([sensor, offset]) => + db.collection('sensorOffsets').doc(sensor).set({ offset, timestamp: new Date().toISOString() }) + ); + + if (calibrationBaseline !== null) { + updates.push(db.collection('sensorOffsets').doc('calibrationBaseline').set({ + baseline: calibrationBaseline, + timestamp: new Date().toISOString() + })); + } + + Promise.all(updates) + .then(() => console.log('Calibration data saved.')) + .catch(error => { + console.error('Save error:', error); + showErrorAlert('Failed to save calibration data.'); + }); +} + document.getElementById('calibrateBtn').addEventListener('click', async () => { const { isConfirmed } = await Swal.fire({ - theme: 'dark', + background: '#1e293b', title: 'Preparation', - text: 'Please place all sensors in the same position for calibration.', + text: 'Please place all sensors together.', icon: 'info', confirmButtonText: 'Ready', allowOutsideClick: false, - allowEscapeKey: false, + allowEscapeKey: false }); - if (!isConfirmed) return; // If user cancels, do nothing + if (!isConfirmed) return; + Swal.fire({ - theme: 'dark', + background: '#1e293b', title: 'Calibrating...', - didOpen: () => { - Swal.showLoading(); - }, + didOpen: () => Swal.showLoading(), allowOutsideClick: false, allowEscapeKey: false }); - const values = Array.from(sensorDataMap.values()); - if (values.length === 0) { - Swal.fire({ - theme: 'dark', - icon: 'error', - title: 'Oops...', - text: 'No sensor data available for calibration.', - timer: 3000, - showConfirmButton: false, - timerProgressBar: true - }); - return; - } + const temps = [...sensorDataMap.values()]; + if (!temps.length) return showErrorAlert('No sensor data found.'); - const sorted = [...values].sort((a, b) => a - b); - const middle = Math.floor(sorted.length / 2); - const Median = sorted.length % 2 !== 0 - ? sorted[middle] - : (sorted[middle - 1] + sorted[middle]) / 2; + const sorted = temps.sort((a, b) => a - b); + const median = sorted.length % 2 + ? sorted[Math.floor(sorted.length / 2)] + : (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2; sensorOffsetMap.clear(); for (const [sensor, temp] of sensorDataMap.entries()) { - sensorOffsetMap.set(sensor, Median - temp); // To Save The Offset for every Sensor! + sensorOffsetMap.set(sensor, median - temp); } - calibrationBaseline = Median; + + calibrationBaseline = median; chart.getDatasetMeta(0).hidden = true; chart.getDatasetMeta(2).hidden = false; - const allOffsetsAreZero = Array.from(sensorOffsetMap.values()).every(offset => Math.abs(offset) < 0.1); + saveOffsetsToFirestore(); - if (allOffsetsAreZero) { - Swal.update({ - html: ` -

Analyzing sensor data...

-

Fun Fact: All your sensors are already in great sync! 🎯

- ` - }); - } - setTimeout(() => { // simulate a bit of delay + const allZero = [...sensorOffsetMap.values()].every(o => Math.abs(o) < 0.1); + + setTimeout(() => { Swal.fire({ - theme: 'dark', + background: '#1e293b', icon: 'success', title: 'Calibration Complete!', html: ` -

Baseline set to ${calibrationBaseline.toFixed(2)}°C

-

Important: Now place all sensors in their appropriate positions as usual.

- ${allOffsetsAreZero ? `

Fun fact: All your sensors were already reporting nearly identical temperatures! 📟

` : ''} - `, - confirmButtonText: 'Got it!', +

Baseline set to ${median.toFixed(2)}°C

+

Offsets saved to Firestore.

+ ${allZero ? `

Sensors were already aligned 🎯

` : ''} + `, + confirmButtonText: 'Done' }); }, 1500); }); - - +// MQTT Client Events client.on('connect', () => { mqttStatus.textContent = 'MQTT: Connected'; - mqttStatus.classList.remove('text-red-500'); - mqttStatus.classList.add('text-green-400'); - - for (let i = 1; i <= 10; i++) { - client.subscribe(`tempro/sensor${i}`); - } + mqttStatus.className = 'text-green-400'; + for (let i = 1; i <= 10; i++) client.subscribe(`tempro/sensor${i}`); }); client.on('message', (topic, message) => { const sensor = topic.split('/')[1]; const temp = parseFloat(message.toString()); + const timestamp = new Date(); if (!isNaN(temp)) { sensorDataMap.set(sensor, temp); - sensorLastSeenMap.set(sensor, Date.now()); + sensorLastSeenMap.set(sensor, timestamp.getTime()); + updateChart(); + updateLastSeenTable(); // 👈 Added here + + db.collection('sensorLastSeen').doc(sensor).set({ + temperature: temp, + lastSeen: timestamp.toISOString() + }, { merge: true }).catch(error => { + console.error(`Failed to update lastSeen for ${sensor}:`, error); + showErrorAlert(`Firestore error: Could not save last seen for ${sensor}`); + }); } }); client.on('error', () => { mqttStatus.textContent = 'MQTT: Error'; - mqttStatus.classList.remove('text-green-400'); - mqttStatus.classList.add('text-red-500'); + mqttStatus.className = 'text-red-500'; }); +// Start +loadOffsetsFromFirestore(); setInterval(updateChart, 5000); +setInterval(updateTemperatureLogs, 20000); diff --git a/Web/style.css b/Web/style.css index 7361e91..ba4aefc 100644 --- a/Web/style.css +++ b/Web/style.css @@ -7,15 +7,13 @@ body { font-family: 'Orbitron', sans-serif; font-weight: 300; letter-spacing: 0.5px; - overflow: hidden; - height: 100%; display: flex; align-items: center; justify-content: center; } /* Container styling */ -.main-container { +.main-container .analytic-container{ width: 100%; max-width: 80rem; height: 80vh;