-
🌡️ Live Temperature Monitor
- MQTT: Connecting... + + +
+
+ ${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: `
-
+
+
+ 🌡️ Live Temperature Monitor
+ MQTT: Connecting... +
+ Status: Normal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📅 Sensor Last Seen
+
+
+
+
+
+ | Sensor | +Temp | +Seen | +
|---|
+
-
+
+
🗂️ Temperature Logs
+ +Status: Normal
-
-
+
-
+
\ 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 = `
+
+
-
📊 Analytics
+
+
+
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;