Created
January 14, 2025 16:34
-
-
Save penyt/95a0b903a236dcc9feac0b5c8663395a to your computer and use it in GitHub Desktop.
pi-pen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Pi Temperature Monitor</title> | |
| <script src="/socket.io/socket.io.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| body { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| font-family: Arial, sans-serif; | |
| overflow-x: hidden; /* no horizontal scoll bar */ | |
| } | |
| h1 { | |
| text-align: center; | |
| } | |
| #temperature { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| } | |
| #chartContainer { | |
| position: relative; | |
| width: 100%; | |
| max-width: 600px; | |
| height: auto; | |
| aspect-ratio: 16 / 9; | |
| margin-bottom: 20px; | |
| } | |
| #chartTitle { | |
| text-align: center; | |
| font-size: 18px; | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| } | |
| canvas { | |
| width: 100% !important; | |
| height: 100% !important; | |
| } | |
| #timeRangeSelector { | |
| position: absolute; | |
| top: 40px; | |
| left: calc(100% + 10px); | |
| z-index: 10; | |
| padding: 5px; | |
| } | |
| .timeRangeSelectorAbove { | |
| position: static; | |
| margin-bottom: 10px; | |
| display: block; | |
| } | |
| @media (max-width: 900px) { | |
| #timeRangeSelector { | |
| position: static; | |
| margin-bottom: 10px; | |
| display: block; | |
| } | |
| #chartContainer { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Temperature Monitor</h1> | |
| <div id="temperature">Loading temperature data...</div> | |
| <div id="chartContainer"> | |
| <select id="timeRangeSelector"> | |
| <option value="1">Last 1 minute</option> | |
| <option value="10">Last 10 minutes</option> | |
| <option value="60">Last 1 hour</option> | |
| <option value="360">Last 6 hours</option> | |
| <option value="1440">Last 24 hours</option> | |
| </select> | |
| <div id="chartTitle">CPU Temperature</div> | |
| <canvas id="temperatureChart"></canvas> | |
| </div> | |
| <script> | |
| const socket = io(); | |
| // set chart | |
| const ctx = document.getElementById('temperatureChart').getContext('2d'); | |
| const temperatureChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: 'CPU Temperature (°C)', | |
| data: [], | |
| borderColor: 'rgba(255, 99, 132, 1)', | |
| borderWidth: 2, | |
| fill: false, | |
| tension: 0.1, | |
| spanGaps: false, | |
| pointRadius: 0, // only line, no point | |
| pointHoverRadius: 0 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| x: { | |
| title: { | |
| display: true, | |
| text: 'Time', | |
| padding: { top: 20 } // "time" padding | |
| }, | |
| grid: { | |
| drawOnChartArea: true, | |
| drawTicks: false, | |
| borderDash: [4, 4] | |
| }, | |
| ticks: { | |
| maxTicksLimit: 20, | |
| padding: 10 | |
| } | |
| }, | |
| y: { | |
| beginAtZero: true, // start from 0 °C | |
| max: 100, | |
| title: { | |
| display: true, | |
| text: 'Temperature (°C)', | |
| padding: { right: 10 } | |
| }, | |
| grid: { | |
| drawOnChartArea: true, | |
| drawTicks: false, | |
| borderDash: [4, 4] | |
| }, | |
| ticks: { | |
| padding: 10 | |
| } | |
| } | |
| }, | |
| plugins: { | |
| legend: { | |
| display: false // no legend | |
| } | |
| } | |
| } | |
| }); | |
| // updating | |
| socket.on('temperatureUpdate', (data) => { | |
| document.getElementById('temperature').innerText = 'Current Temperature: ' + data.temperature + ' °C'; | |
| const currentTime = new Date().toLocaleTimeString('en-US'); | |
| if (temperatureChart.data.labels.length > 288) { | |
| temperatureChart.data.labels.shift(); | |
| temperatureChart.data.datasets[0].data.shift(); | |
| } | |
| temperatureChart.data.labels.push(currentTime); | |
| temperatureChart.data.datasets[0].data.push(data.temperature); | |
| temperatureChart.update(); | |
| }); | |
| // time range | |
| document.getElementById('timeRangeSelector').addEventListener('change', function () { | |
| const selectedValue = parseInt(this.value); | |
| updateChart(selectedValue); | |
| }); | |
| // updating chart | |
| function updateChart(minutes) { | |
| fetch('/api/temperature/history') | |
| .then(response => response.json()) | |
| .then(data => { | |
| const now = Date.now(); | |
| const filteredData = []; | |
| const labels = []; | |
| // set start time | |
| const startTime = now - minutes * 60 * 1000; | |
| // update every 5 sec | |
| for (let time = startTime; time <= now; time += 5000) { | |
| const dataPoint = data.find(item => Math.abs(item.time - time) < 2500); // 寬容時間差,找出最接近的數據點 | |
| labels.push(new Date(time).toLocaleTimeString()); | |
| if (dataPoint) { | |
| filteredData.push(dataPoint.temperature); | |
| } else { | |
| filteredData.push(null); // nodata | |
| } | |
| } | |
| temperatureChart.data.labels = labels; | |
| temperatureChart.data.datasets[0].data = filteredData; | |
| temperatureChart.update(); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const express = require('express'); | |
| const { Server } = require('socket.io'); | |
| const http = require('http'); | |
| const fs = require('fs'); | |
| const path = './temperatureData.json'; | |
| const { execSync } = require('child_process'); | |
| const app = express(); | |
| const server = http.createServer(app); | |
| const io = new Server(server); | |
| // frontend | |
| app.use(express.static('public')); | |
| // initialize data | |
| let temperatureData = []; | |
| if (fs.existsSync(path)) { | |
| try { | |
| const fileData = fs.readFileSync(path, 'utf8'); | |
| temperatureData = JSON.parse(fileData) || []; | |
| } catch (err) { | |
| console.error('Failed to read data from file:', err); | |
| } | |
| } | |
| // get temperature every 5 sec | |
| setInterval(() => { | |
| const temperature = getCPUTemperature(); | |
| const currentTime = Date.now(); | |
| temperatureData.push({ time: currentTime, temperature }); // formatting | |
| const oneDayAgo = currentTime - 24 * 60 * 60 * 1000; // 24hr data at most | |
| temperatureData = temperatureData.filter(data => data.time > oneDayAgo); | |
| // write into json file | |
| try { | |
| fs.writeFileSync(path, JSON.stringify(temperatureData), 'utf8'); | |
| } catch (err) { | |
| console.error('Failed to save data to file:', err); | |
| } | |
| }, 5000); | |
| // get temperature | |
| app.get('/api/temperature/history', (req, res) => { | |
| res.json(temperatureData); | |
| }); | |
| // WebSocket 通信用於實時溫度監控 | |
| io.on('connection', (socket) => { | |
| console.log('Client is connected'); | |
| // send data every 5 sec | |
| const interval = setInterval(() => { | |
| if (temperatureData.length > 0) { | |
| const latestTemperature = temperatureData[temperatureData.length - 1]; | |
| socket.emit('temperatureUpdate', { temperature: latestTemperature.temperature }); | |
| } | |
| }, 5000); | |
| socket.on('disconnect', () => { | |
| clearInterval(interval); | |
| console.log('Client loses connection :('); | |
| }); | |
| }); | |
| // get Pi temperature | |
| //function getCPUTemperature() { | |
| // return Math.floor(Math.random() * (60 - 30 + 1)) + 30; | |
| //} | |
| function getCPUTemperature() { | |
| try { | |
| // 執行 vcgencmd 命令獲取 CPU 溫度 | |
| const tempOutput = execSync('vcgencmd measure_temp').toString(); | |
| const tempMatch = tempOutput.match(/temp=([\d.]+)/); // 使用正規表達式抓取溫度數值 | |
| if (tempMatch && tempMatch[1]) { | |
| return parseFloat(tempMatch[1]); // 轉換成浮點數並返回 | |
| } else { | |
| console.error('Failed to parse CPU temperature.'); | |
| return 0; // 若解析失敗,返回一個默認值 | |
| } | |
| } catch (error) { | |
| console.error('Error reading CPU temperature:', error.message); | |
| return 0; // 如果命令執行失敗,返回一個默認值 | |
| } | |
| } | |
| // start on port 5472 | |
| const PORT = process.env.PORT || 5472; | |
| server.listen(PORT, () => { | |
| console.log(`server is running at http://localhost:${PORT}`); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const express = require('express'); | |
| const { Server } = require('socket.io'); | |
| const http = require('http'); | |
| const fs = require('fs'); | |
| const path = './temperatureData.json'; | |
| const { execSync } = require('child_process'); | |
| const app = express(); | |
| const server = http.createServer(app); | |
| const io = new Server(server); | |
| // frontend | |
| app.use(express.static('public')); | |
| // initialize data | |
| let temperatureData = []; | |
| if (fs.existsSync(path)) { | |
| try { | |
| const fileData = fs.readFileSync(path, 'utf8'); | |
| temperatureData = JSON.parse(fileData) || []; | |
| } catch (err) { | |
| console.error('Failed to read data from file:', err); | |
| } | |
| } | |
| // get temperature every 5 sec | |
| setInterval(() => { | |
| const temperature = getCPUTemperature(); | |
| const currentTime = Date.now(); | |
| temperatureData.push({ time: currentTime, temperature }); // formatting | |
| const oneDayAgo = currentTime - 24 * 60 * 60 * 1000; // 24hr data at most | |
| temperatureData = temperatureData.filter(data => data.time > oneDayAgo); | |
| // write into json file | |
| try { | |
| fs.writeFileSync(path, JSON.stringify(temperatureData), 'utf8'); | |
| } catch (err) { | |
| console.error('Failed to save data to file:', err); | |
| } | |
| }, 5000); | |
| // get temperature | |
| app.get('/api/temperature/history', (req, res) => { | |
| res.json(temperatureData); | |
| }); | |
| // WebSocket 通信用於實時溫度監控 | |
| io.on('connection', (socket) => { | |
| console.log('Client is connected'); | |
| // send data every 5 sec | |
| const interval = setInterval(() => { | |
| if (temperatureData.length > 0) { | |
| const latestTemperature = temperatureData[temperatureData.length - 1]; | |
| socket.emit('temperatureUpdate', { temperature: latestTemperature.temperature }); | |
| } | |
| }, 5000); | |
| socket.on('disconnect', () => { | |
| clearInterval(interval); | |
| console.log('Client loses connection :('); | |
| }); | |
| }); | |
| // get Pi temperature | |
| //function getCPUTemperature() { | |
| // return Math.floor(Math.random() * (60 - 30 + 1)) + 30; | |
| //} | |
| function getCPUTemperature() { | |
| try { | |
| console.log('Attempting to read /sys/class/thermal/thermal_zone0/temp...'); | |
| const temp = fs.readFileSync('/sys/class/thermal/thermal_zone0/temp', 'utf8'); | |
| return parseFloat(temp) / 1000; // 轉換為攝氏溫度 | |
| } catch (error) { | |
| console.error('Failed to read CPU temperature:', error.message); | |
| return 0; | |
| } | |
| } | |
| // start on port 5472 | |
| const PORT = process.env.PORT || 5472; | |
| server.listen(PORT, () => { | |
| console.log(`server is running at http://localhost:${PORT}`); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment