時溫濕度監(jiān)控系統(tǒng)搭建指南)
1. 項目緣起為什么要在樹莓派上折騰實(shí)時溫濕度圖表幾年前我接手了一個小項目需要在辦公室的幾個角落實(shí)時監(jiān)控溫濕度的變化。市面上現(xiàn)成的物聯(lián)網(wǎng)傳感器要么太貴要么數(shù)據(jù)平臺封閉要么就是數(shù)據(jù)刷新慢得讓人著急。當(dāng)時我就想能不能自己動手用最少的成本搭建一個數(shù)據(jù)實(shí)時可見、歷史可查、還能在局域網(wǎng)內(nèi)任何設(shè)備上隨時查看的系統(tǒng)這個想法最終落地成了一個基于樹莓派、WebSocket和動態(tài)圖表的實(shí)時監(jiān)控方案。它聽起來有點(diǎn)技術(shù)棧但拆解開來核心就三塊一個負(fù)責(zé)采集數(shù)據(jù)的樹莓派Raspberry Pi一個負(fù)責(zé)實(shí)時推送數(shù)據(jù)的通信協(xié)議WebSocket和一個負(fù)責(zé)把數(shù)據(jù)畫得漂亮的前端圖表庫Charts。今天我就把這個從零搭建的過程以及其中踩過的坑、總結(jié)的經(jīng)驗(yàn)完整地分享出來。無論你是想監(jiān)控家里的花房、實(shí)驗(yàn)室的環(huán)境還是想學(xué)習(xí)如何將硬件數(shù)據(jù)與Web應(yīng)用實(shí)時結(jié)合這篇內(nèi)容都能給你一份可以直接“抄作業(yè)”的指南。整個系統(tǒng)的邏輯非常直觀樹莓派上連接著溫濕度傳感器比如經(jīng)典的DHT22它定時讀取數(shù)據(jù)同時樹莓派運(yùn)行著一個Python Web服務(wù)器這個服務(wù)器一方面通過WebSocket與訪問網(wǎng)頁的瀏覽器保持長連接另一方面將讀取到的傳感器數(shù)據(jù)實(shí)時地“推”給所有正在查看的瀏覽器瀏覽器端則用JavaScript圖表庫比如Chart.js接收這些數(shù)據(jù)并動態(tài)地繪制出實(shí)時變化的曲線圖。這樣一來你只要在手機(jī)或電腦的瀏覽器里輸入樹莓派的IP地址就能看到一個不斷更新的溫濕度儀表盤。2. 硬件選型與樹莓派基礎(chǔ)環(huán)境搭建工欲善其事必先利其器。硬件是數(shù)據(jù)的源頭環(huán)境是系統(tǒng)穩(wěn)定運(yùn)行的基石。這一部分我們來搞定硬件連接和最精簡、穩(wěn)定的軟件環(huán)境。2.1 傳感器與樹莓派的選型考量對于溫濕度監(jiān)測DHT11和DHT22是最常見的選擇。DHT11價格極低但精度也相對較低濕度±5%溫度±2℃刷新速度慢最快1秒一次。DHT22價格稍高但精度濕度±2-5%溫度±0.5℃和響應(yīng)速度最快2秒一次要好得多。對于環(huán)境監(jiān)控尤其是需要觀察細(xì)微變化的場景我強(qiáng)烈推薦直接上DHT22多花的那點(diǎn)錢在數(shù)據(jù)質(zhì)量上完全值得。樹莓派方面從Zero W到最新的Pi 5都能勝任。我的建議是如果項目需要7x24小時長期運(yùn)行并且可能在未來擴(kuò)展其他功能比如加個攝像頭那么至少選擇樹莓派3B或4B它們有更強(qiáng)的處理能力和更好的穩(wěn)定性。如果追求極致的低功耗和迷你體積樹莓派Zero W帶無線也完全足夠運(yùn)行我們這個輕量級的Web服務(wù)。我這次演示用的是手頭閑置的樹莓派4B 2GB版本。連接非常簡單。DHT22有三個引腳有的版本是四個但通常第四個是空腳VCC3.3V、DATA、GND。VCC連接到樹莓派的3.3V引腳例如物理引腳1或17。DATA連接到樹莓派任意一個GPIO引腳比如GPIO4物理引腳7。GND連接到樹莓派的任意GND引腳例如物理引腳6、9、14、20等。注意務(wù)必確認(rèn)連接到3.3V而不是5V。樹莓派的GPIO引腳耐壓是3.3V接5V可能會永久損壞你的樹莓派。另外DATA信號線最好連接一個4.7kΩ - 10kΩ的上拉電阻到3.3V以確保信號穩(wěn)定。很多DHT22模塊已經(jīng)集成了這個電阻購買時可以留意。2.2 操作系統(tǒng)與Python環(huán)境配置首先為樹莓派安裝一個輕量級的操作系統(tǒng)。Raspberry Pi OS Lite無桌面版是最佳選擇它資源占用少更適合作為服務(wù)器長期運(yùn)行。使用Raspberry Pi Imager工具刷寫系統(tǒng)到SD卡時記得在設(shè)置中預(yù)先開啟SSH并配置好Wi-Fi這樣你就能通過命令行遠(yuǎn)程訪問無需連接顯示器和鍵盤。系統(tǒng)啟動后第一件事是更新軟件包列表并升級現(xiàn)有軟件這是一個好習(xí)慣sudo apt update sudo apt full-upgrade -y我們的核心服務(wù)將用Python編寫因此需要確保Python3和pipPython包管理器已就位。樹莓派OS通常預(yù)裝了Python3但我們需要安裝pip和一些必要的開發(fā)工具sudo apt install -y python3-pip python3-venv接下來我強(qiáng)烈建議為這個項目創(chuàng)建一個獨(dú)立的Python虛擬環(huán)境。這能避免項目依賴包污染系統(tǒng)全局環(huán)境也便于管理和遷移。mkdir ~/temp_humidity_project cd ~/temp_humidity_project python3 -m venv venv source venv/bin/activate當(dāng)你看到命令行提示符前出現(xiàn)(venv)字樣就說明虛擬環(huán)境激活成功了。后續(xù)的所有Python包安裝都應(yīng)該在這個激活的環(huán)境中進(jìn)行。2.3 核心Python依賴庫安裝我們的項目需要幾個關(guān)鍵的Python庫Adafruit_DHT這是讀取DHT22傳感器數(shù)據(jù)最常用、最穩(wěn)定的庫之一。Flask一個輕量級的Python Web框架我們將用它來搭建Web服務(wù)器和提供WebSocket支持。Flask-SocketIO為Flask應(yīng)用提供WebSocket功能的擴(kuò)展庫它是我們實(shí)現(xiàn)實(shí)時數(shù)據(jù)推送的核心。在激活的虛擬環(huán)境中使用pip安裝它們(venv) pip install Adafruit_DHT Flask Flask-SocketIO安裝完成后可以寫一個簡單的測試腳本來驗(yàn)證傳感器和庫是否工作正常。創(chuàng)建一個名為test_sensor.py的文件import Adafruit_DHT import time # 設(shè)置傳感器類型和GPIO引腳 SENSOR Adafruit_DHT.DHT22 PIN 4 # 我們之前連接的GPIO4 try: while True: humidity, temperature Adafruit_DHT.read_retry(SENSOR, PIN) if humidity is not None and temperature is not None: print(f溫度: {temperature:.1f}°C, 濕度: {humidity:.1f}%) else: print(讀取傳感器失敗請檢查連接。) time.sleep(2) # 每2秒讀取一次 except KeyboardInterrupt: print(程序停止)運(yùn)行這個腳本python test_sensor.py如果能看到周期性的溫濕度輸出恭喜你硬件和基礎(chǔ)環(huán)境搭建成功3. 構(gòu)建后端Flask與WebSocket服務(wù)器數(shù)據(jù)能讀出來了接下來我們要搭建一個“中轉(zhuǎn)站”它既能通過網(wǎng)頁提供訪問界面又能通過WebSocket把數(shù)據(jù)實(shí)時推出去。Flask加上SocketIO的組合讓這件事變得異常簡單。3.1 Flask應(yīng)用骨架與靜態(tài)文件服務(wù)首先我們在項目根目錄創(chuàng)建主應(yīng)用文件app.py。Flask應(yīng)用的基本結(jié)構(gòu)如下from flask import Flask, render_template from flask_socketio import SocketIO, emit import Adafruit_DHT from threading import Thread, Event import time import json # 初始化Flask和SocketIO app Flask(__name__) app.config[SECRET_KEY] your_secret_key_here # 用于會話安全生產(chǎn)環(huán)境請使用強(qiáng)密鑰 socketio SocketIO(app, cors_allowed_origins*) # 允許所有來源跨域開發(fā)時方便 # 傳感器配置 SENSOR Adafruit_DHT.DHT22 SENSOR_PIN 4 # 用于控制后臺數(shù)據(jù)采集線程 thread None thread_stop_event Event() def background_sensor_thread(): 后臺線程持續(xù)讀取傳感器數(shù)據(jù)并通過WebSocket廣播 print(傳感器數(shù)據(jù)采集線程啟動) while not thread_stop_event.is_set(): humidity, temperature Adafruit_DHT.read_retry(SENSOR, SENSOR_PIN) if humidity is not None and temperature is not None: # 準(zhǔn)備數(shù)據(jù) sensor_data { temperature: round(temperature, 1), humidity: round(humidity, 1), timestamp: time.time() * 1000 # 轉(zhuǎn)換為JavaScript常用的毫秒時間戳 } # 通過SocketIO的emit方法廣播數(shù)據(jù)到所有連接的客戶端 # broadcastTrue確保所有打開頁面的用戶都能收到 socketio.emit(new_sensor_data, json.dumps(sensor_data), broadcastTrue) print(f廣播數(shù)據(jù): {sensor_data}) else: print(讀取傳感器數(shù)據(jù)失敗本次跳過。) time.sleep(2) # 每2秒采集并推送一次數(shù)據(jù) app.route(/) def index(): 主頁面路由返回HTML return render_template(index.html) socketio.on(connect) def handle_connect(): 當(dāng)WebSocket客戶端連接時觸發(fā) global thread print(客戶端已連接) # 確保只有一個后臺線程在運(yùn)行 if thread is None: thread Thread(targetbackground_sensor_thread) thread.start() socketio.on(disconnect) def handle_disconnect(): 當(dāng)WebSocket客戶端斷開時觸發(fā) print(客戶端斷開連接) if __name__ __main__: # 啟動服務(wù)器監(jiān)聽所有網(wǎng)絡(luò)接口的5000端口 # allow_unsafe_werkzeugTrue 是Flask-SocketIO在調(diào)試模式下的一個選項 socketio.run(app, host0.0.0.0, port5000, debugTrue, allow_unsafe_werkzeugTrue)這段代碼做了幾件關(guān)鍵的事創(chuàng)建了Flask應(yīng)用和SocketIO實(shí)例。定義了一個后臺線程background_sensor_thread它在一個循環(huán)中不斷讀取傳感器數(shù)據(jù)并使用socketio.emit將數(shù)據(jù)以new_sensor_data為事件名廣播給所有連接的Web客戶端。定義了根路由/返回一個HTML頁面我們稍后創(chuàng)建templates/index.html。定義了WebSocket的連接 (connect) 和斷開 (disconnect) 事件處理函數(shù)并在第一個客戶端連接時啟動后臺數(shù)據(jù)采集線程。3.2 處理WebSocket連接與數(shù)據(jù)廣播的細(xì)節(jié)這里有幾個容易出錯的細(xì)節(jié)需要展開講講。首先是socketio.run(app, ...)這行。在開發(fā)時我們通常用Flask自帶的開發(fā)服務(wù)器啟動但Flask-SocketIO為了處理WebSocket需要使用它自己封裝過的run方法而不是app.run()。參數(shù)host0.0.0.0至關(guān)重要它讓服務(wù)器監(jiān)聽所有網(wǎng)絡(luò)接口這樣你才能在同一局域網(wǎng)下的其他設(shè)備如手機(jī)、電腦上通過樹莓派的IP地址訪問這個服務(wù)。如果只監(jiān)聽127.0.0.1或localhost那就只有樹莓派本機(jī)可以訪問了。其次是跨域問題。在初始化SocketIO時我設(shè)置了cors_allowed_origins*。這在開發(fā)和測試階段沒問題意味著任何來源的網(wǎng)頁都可以連接這個WebSocket。但在生產(chǎn)環(huán)境或?qū)Π踩幸髸r你應(yīng)該將其設(shè)置為具體的域名或IP例如cors_allowed_origins[http://192.168.1.100:5000]以防止不受信任的網(wǎng)站連接你的數(shù)據(jù)流。后臺線程的設(shè)計是為了避免阻塞主Web服務(wù)器。傳感器讀取 (Adafruit_DHT.read_retry) 可能會有短暫的延遲如果放在Web請求處理函數(shù)里會拖慢整個服務(wù)器的響應(yīng)。用一個獨(dú)立的線程在后臺默默工作主服務(wù)器只負(fù)責(zé)接收HTTP請求和推送WebSocket事件這樣架構(gòu)更清晰、性能更好。thread_stop_event是一個線程事件對象用于在服務(wù)器關(guān)閉時優(yōu)雅地停止后臺線程雖然上面的示例代碼沒有實(shí)現(xiàn)關(guān)閉邏輯但在更完善的服務(wù)中需要處理。4. 打造前端動態(tài)圖表與實(shí)時數(shù)據(jù)綁定后端準(zhǔn)備好了持續(xù)噴涌的數(shù)據(jù)流前端就需要一個容器來接收并漂亮地展示它。我們將創(chuàng)建一個簡單的HTML頁面引入Chart.js來繪制圖表并使用Socket.IO的客戶端庫來建立WebSocket連接。4.1 基礎(chǔ)HTML結(jié)構(gòu)與Chart.js初始化在項目根目錄下創(chuàng)建templates文件夾然后在里面創(chuàng)建index.html。這是我們的前端界面。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title樹莓派溫濕度實(shí)時監(jiān)控/title script srchttps://cdn.jsdelivr.net/npm/chart.js/script script srchttps://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.0/socket.io.min.js/script style body { font-family: sans-serif; margin: 20px; background-color: #f5f5f5; } .container { max-width: 1000px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .chart-container { position: relative; height: 400px; margin-bottom: 30px; } .status { margin-bottom: 20px; padding: 10px; border-radius: 5px; background-color: #e8f5e9; border-left: 4px solid #4caf50; } .data-display { display: flex; justify-content: space-around; margin-bottom: 20px; } .data-box { text-align: center; padding: 15px; border-radius: 8px; min-width: 120px; } .temp-box { background-color: #ffebee; border: 2px solid #ef5350; } .humi-box { background-color: #e8f5e9; border: 2px solid #66bb6a; } .value { font-size: 2.5em; font-weight: bold; } .unit { font-size: 1em; color: #666; } .label { font-size: 1.2em; margin-bottom: 5px; } /style /head body div classcontainer h1? 樹莓派溫濕度實(shí)時監(jiān)控儀表板/h1 div classstatus idconnectionStatus 正在連接數(shù)據(jù)服務(wù)器... /div div classdata-display div classdata-box temp-box div classlabel當(dāng)前溫度/div div classvalue idcurrentTemp--/div div classunit°C/div /div div classdata-box humi-box div classlabel當(dāng)前濕度/div div classvalue idcurrentHumi--/div div classunit%/div /div /div h2歷史趨勢圖/h2 div classchart-container canvas idsensorChart/canvas /div /div script // 頁面加載完成后執(zhí)行的代碼 document.addEventListener(DOMContentLoaded, function() { // 1. 初始化圖表 const ctx document.getElementById(sensorChart).getContext(2d); let chart new Chart(ctx, { type: line, data: { labels: [], // 時間標(biāo)簽初始為空 datasets: [{ label: 溫度 (°C), data: [], // 溫度數(shù)據(jù)點(diǎn) borderColor: rgb(255, 99, 132), backgroundColor: rgba(255, 99, 132, 0.1), tension: 0.4, // 曲線平滑度 yAxisID: y, }, { label: 濕度 (%), data: [], // 濕度數(shù)據(jù)點(diǎn) borderColor: rgb(54, 162, 235), backgroundColor: rgba(54, 162, 235, 0.1), tension: 0.4, yAxisID: y1, }] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: index, intersect: false }, scales: { x: { title: { display: true, text: 時間 }, ticks: { maxTicksLimit: 10 } }, y: { type: linear, display: true, position: left, title: { display: true, text: 溫度 (°C) }, suggestedMin: 10, suggestedMax: 40 }, y1: { type: linear, display: true, position: right, title: { display: true, text: 濕度 (%) }, suggestedMin: 20, suggestedMax: 80, // 確保右側(cè)濕度軸與左側(cè)溫度軸的網(wǎng)格線不重疊 grid: { drawOnChartArea: false }, } }, plugins: { legend: { display: true }, tooltip: { mode: index, intersect: false } } } }); // 2. 連接WebSocket服務(wù)器 // 注意這里的地址需要替換成你樹莓派的實(shí)際IP // 例如const socket io(http://192.168.1.100:5000); const socket io(); // 如果前端和后端在同一主機(jī)和端口可以這樣簡寫 // 3. 更新連接狀態(tài) socket.on(connect, function() { console.log(WebSocket連接成功); document.getElementById(connectionStatus).innerHTML ? 已連接到數(shù)據(jù)服務(wù)器正在接收實(shí)時數(shù)據(jù)...; document.getElementById(connectionStatus).style.backgroundColor #e8f5e9; document.getElementById(connectionStatus).style.borderLeftColor #4caf50; }); socket.on(disconnect, function() { console.log(WebSocket連接斷開。); document.getElementById(connectionStatus).innerHTML ? 數(shù)據(jù)服務(wù)器連接已斷開正在嘗試重連...; document.getElementById(connectionStatus).style.backgroundColor #ffebee; document.getElementById(connectionStatus).style.borderLeftColor #ef5350; }); // 4. 監(jiān)聽后端發(fā)來的new_sensor_data事件 socket.on(new_sensor_data, function(dataStr) { const data JSON.parse(dataStr); console.log(收到新數(shù)據(jù):, data); // 更新頂部當(dāng)前值顯示 document.getElementById(currentTemp).textContent data.temperature.toFixed(1); document.getElementById(currentHumi).textContent data.humidity.toFixed(1); // 將新數(shù)據(jù)點(diǎn)添加到圖表中 const timeLabel new Date(data.timestamp).toLocaleTimeString(); // 轉(zhuǎn)換為本地時間字符串 chart.data.labels.push(timeLabel); chart.data.datasets[0].data.push(data.temperature); chart.data.datasets[1].data.push(data.humidity); // 限制圖表上顯示的數(shù)據(jù)點(diǎn)數(shù)量避免過于擁擠例如只保留最近60個點(diǎn) const maxDataPoints 60; if (chart.data.labels.length maxDataPoints) { chart.data.labels.shift(); // 移除最舊的時間標(biāo)簽 chart.data.datasets[0].data.shift(); // 移除最舊的溫度數(shù)據(jù) chart.data.datasets[1].data.shift(); // 移除最舊的濕度數(shù)據(jù) } // 更新圖表觸發(fā)重繪 chart.update(none); // 參數(shù)‘none’表示不應(yīng)用動畫更新更流暢 }); }); /script /body /html4.2 圖表配置與數(shù)據(jù)更新的核心邏輯前端代碼的核心是Chart.js的配置和Socket.IO的事件監(jiān)聽。在圖表配置中我創(chuàng)建了兩個Y軸y和y1分別位于左右兩側(cè)用于顯示單位和量綱都不同的溫度和濕度。tension: 0.4讓曲線變得平滑觀感更好。scales配置中的suggestedMin和suggestedMax為坐標(biāo)軸提供了一個初始的建議范圍Chart.js會根據(jù)實(shí)際數(shù)據(jù)的進(jìn)入動態(tài)調(diào)整但這給了圖表一個良好的初始狀態(tài)。數(shù)據(jù)更新邏輯是前端的靈魂。當(dāng)socket.on(new_sensor_data)觸發(fā)時我們解析數(shù)據(jù)從WebSocket收到的JSON字符串中解析出溫度、濕度和時間戳。更新數(shù)值顯示立即更新頁面頂部兩個大數(shù)字給用戶最直觀的反饋。追加圖表數(shù)據(jù)將新的數(shù)據(jù)點(diǎn)溫度、濕度和時間標(biāo)簽分別推入push圖表對應(yīng)的數(shù)據(jù)數(shù)組中。限制數(shù)據(jù)量這是一個非常重要的優(yōu)化。如果不加限制圖表數(shù)據(jù)數(shù)組會無限增長最終導(dǎo)致瀏覽器內(nèi)存占用過高、圖表渲染卡頓。這里我們設(shè)定只保留最近60個數(shù)據(jù)點(diǎn)即最近2分鐘的數(shù)據(jù)假設(shè)2秒一個點(diǎn)。當(dāng)數(shù)組長度超過60時使用shift()方法移除數(shù)組最開頭的元素最舊的數(shù)據(jù)。這樣圖表始終顯示一個“滑動窗口”內(nèi)的最新數(shù)據(jù)。觸發(fā)圖表更新調(diào)用chart.update(none)重繪圖表。參數(shù)none表示禁用更新動畫這樣在快速接收數(shù)據(jù)時圖表的刷新會更流暢沒有視覺上的滯后感。注意前端代碼中const socket io();這行它假設(shè)前端頁面是從與后端服務(wù)器相同的主機(jī):端口加載的。在我們的場景里樹莓派的IP是192.168.1.100服務(wù)跑在5000端口。那么你在同一局域網(wǎng)的電腦上訪問http://192.168.1.100:5000時頁面能正常加載但Socket.IO客戶端庫會默認(rèn)嘗試連接加載頁面的同一個主機(jī)和端口也就是192.168.1.100:5000這是正確的。所以這種簡寫方式在部署后是可行的。但在開發(fā)調(diào)試時如果你的前端文件是直接用瀏覽器打開本地文件file://協(xié)議那么io()會嘗試連接localhost這就會失敗。因此在最終部署時確保HTML頁面是由你的Flask服務(wù)器app.py通過render_template(index.html)提供的這樣它們就同源了。5. 局域網(wǎng)訪問、部署與長期運(yùn)行優(yōu)化代碼都寫好了怎么讓它跑起來并且穩(wěn)定、安全地長期運(yùn)行呢這一步才是從“玩具”到“工具”的關(guān)鍵。5.1 啟動服務(wù)與局域網(wǎng)訪問測試首先確保你在樹莓派上項目虛擬環(huán)境已激活然后運(yùn)行我們的主程序cd ~/temp_humidity_project source venv/bin/activate python app.py你應(yīng)該會看到類似下面的輸出* Serving Flask app app * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. * Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000 * Running on http://192.168.1.100:5000 (此處是SocketIO和Werkzeug的日志) 傳感器數(shù)據(jù)采集線程啟動注意日志中顯示的IP地址例如http://192.168.1.100:5000這就是你樹莓派在局域網(wǎng)內(nèi)的地址。測試步驟樹莓派本地測試在樹莓派本身如果接了顯示器或者通過SSH連接的終端里用文本瀏覽器如lynx或者用curl測試路由是否正常。更簡單的方法是在樹莓派上安裝一個圖形化瀏覽器如sudo apt install chromium-browser然后打開瀏覽器訪問http://127.0.0.1:5000應(yīng)該能看到頁面。同局域網(wǎng)其他設(shè)備測試這是關(guān)鍵。在你的筆記本電腦或手機(jī)上打開瀏覽器輸入http://[樹莓派的IP地址]:5000例如http://192.168.1.100:5000。如果能看到監(jiān)控頁面并且頂部的狀態(tài)提示變成“已連接”溫度濕度數(shù)值開始變化圖表也開始繪制曲線那么恭喜你整個系統(tǒng)成功運(yùn)行了如果無法訪問請按以下步驟排查檢查防火墻樹莓派OS默認(rèn)的防火墻如ufw可能關(guān)閉了5000端口。可以臨時關(guān)閉防火墻測試sudo ufw disable或者開放5000端口sudo ufw allow 5000。檢查IP地址確保你輸入的IP正確。在樹莓派終端輸入hostname -I查看所有IP地址。檢查服務(wù)綁定確認(rèn)app.py中socketio.run(app, host0.0.0.0, ...)已正確設(shè)置。5.2 使用系統(tǒng)服務(wù)實(shí)現(xiàn)開機(jī)自啟與后臺運(yùn)行我們不可能一直開著SSH窗口運(yùn)行python app.py。我們需要讓它作為一個系統(tǒng)服務(wù)在后臺運(yùn)行并且開機(jī)自動啟動。這里我們使用systemd。首先創(chuàng)建一個服務(wù)文件sudo nano /etc/systemd/system/temp_humidity.service將以下內(nèi)容粘貼進(jìn)去注意修改WorkingDirectory和ExecStart的路徑為你項目的實(shí)際路徑以及User為你的樹莓派用戶名通常是pi[Unit] DescriptionTemp Humidity WebSocket Chart Service Afternetwork.target [Service] Typesimple Userpi WorkingDirectory/home/pi/temp_humidity_project EnvironmentPATH/home/pi/temp_humidity_project/venv/bin ExecStart/home/pi/temp_humidity_project/venv/bin/python /home/pi/temp_humidity_project/app.py Restarton-failure RestartSec10 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target這個配置文件告訴系統(tǒng)WorkingDirectory服務(wù)啟動時的工作目錄。Environment設(shè)置環(huán)境變量這里將PATH指向了虛擬環(huán)境的bin目錄確保服務(wù)使用虛擬環(huán)境中的Python和依賴。ExecStart啟動服務(wù)的具體命令即用虛擬環(huán)境中的Python解釋器運(yùn)行我們的app.py。Restarton-failure如果程序異常退出自動重啟。RestartSec10重啟前等待10秒。保存退出后執(zhí)行以下命令# 重新加載systemd配置 sudo systemctl daemon-reload # 啟用服務(wù)使其開機(jī)自啟 sudo systemctl enable temp_humidity.service # 立即啟動服務(wù) sudo systemctl start temp_humidity.service # 查看服務(wù)狀態(tài)確認(rèn)運(yùn)行正常 sudo systemctl status temp_humidity.service如果狀態(tài)顯示為active (running)并且日志里沒有報錯說明服務(wù)啟動成功。現(xiàn)在你可以重啟樹莓派服務(wù)也會自動啟動。要查看實(shí)時日志可以使用sudo journalctl -u temp_humidity.service -f。5.3 性能優(yōu)化與常見問題排查項目運(yùn)行起來后你可能會遇到一些小問題這里分享一些優(yōu)化和排查經(jīng)驗(yàn)。1. 圖表數(shù)據(jù)點(diǎn)過多導(dǎo)致瀏覽器卡頓我們之前在前端代碼里限制了60個數(shù)據(jù)點(diǎn)這對于實(shí)時監(jiān)控通常是足夠的。但如果你的采樣頻率很高比如每秒一次并且需要查看更長時間的歷史趨勢60個點(diǎn)可能不夠。這時可以考慮兩種策略前端數(shù)據(jù)聚合在將數(shù)據(jù)推送給圖表前對歷史數(shù)據(jù)進(jìn)行降采樣。例如每10個原始數(shù)據(jù)點(diǎn)計算一個平均值作為一個顯示點(diǎn)。這需要在前端JavaScript中增加一些邏輯。后端歷史存儲與查詢在后端使用小型數(shù)據(jù)庫如SQLite存儲所有歷史數(shù)據(jù)。前端圖表初始化時先通過一個普通的HTTP API接口請求過去一段時間比如一小時的聚合數(shù)據(jù)來初始化圖表然后WebSocket只負(fù)責(zé)推送最新的實(shí)時數(shù)據(jù)點(diǎn)。這樣圖表既能展示長期趨勢又不會因?yàn)閷?shí)時數(shù)據(jù)流而變得臃腫。2. WebSocket連接不穩(wěn)定或斷開在不太穩(wěn)定的網(wǎng)絡(luò)環(huán)境中WebSocket連接可能會中斷。Socket.IO客戶端庫本身具備自動重連機(jī)制我們代碼中已經(jīng)體現(xiàn)了這一點(diǎn)監(jiān)聽disconnect事件并更新UI狀態(tài)。為了增強(qiáng)健壯性可以在后端增加心跳檢測ping/pong但Flask-SocketIO默認(rèn)已經(jīng)處理了。如果發(fā)現(xiàn)頻繁斷開可以檢查樹莓派的網(wǎng)絡(luò)連接是否穩(wěn)定或者考慮調(diào)整Socket.IO的傳輸方式默認(rèn)會嘗試WebSocket失敗則降級為長輪詢。3. 傳感器讀取失敗或數(shù)據(jù)異常Adafruit_DHT.read_retry函數(shù)本身就有重試機(jī)制但有時會因?yàn)橛布佑|不良、電源干擾等原因持續(xù)失敗。在生產(chǎn)環(huán)境中可以增加更完善的錯誤處理邏輯比如連續(xù)失敗N次后在WebSocket數(shù)據(jù)中增加一個“傳感器故障”的狀態(tài)標(biāo)志前端收到后可以在頁面上顯示警告信息而不是一直顯示舊數(shù)據(jù)或“--”。4. 提升前端體驗(yàn)自動重連提示當(dāng)disconnect事件觸發(fā)時除了改變狀態(tài)框顏色還可以啟動一個定時器每隔幾秒嘗試重新建立連接Socket.IO會自動做但我們可以給用戶更明確的提示。圖表交互Chart.js支持豐富的交互比如鼠標(biāo)懸停顯示精確值、拖動縮放查看歷史細(xì)節(jié)等。這些功能在默認(rèn)配置中已部分開啟你可以根據(jù)文檔進(jìn)一步定制。移動端適配我們使用了viewport和相對單位基礎(chǔ)適配已經(jīng)不錯。對于更復(fù)雜的移動端界面可以考慮使用更響應(yīng)式的CSS框架或者針對小屏幕調(diào)整圖表的maintainAspectRatio等選項。通過以上步驟你不僅擁有了一個可用的實(shí)時溫濕度監(jiān)控系統(tǒng)更掌握了一套將硬件數(shù)據(jù)通過Web技術(shù)實(shí)時可視化的完整方法。這個框架具有很強(qiáng)的擴(kuò)展性你可以輕松地將DHT22傳感器替換成其他GPIO設(shè)備如光照傳感器、土壤濕度傳感器或者在前端增加更多的圖表類型和控件打造屬于你自己的物聯(lián)網(wǎng)監(jiān)控面板。