控系統(tǒng):從ESP32到Grafana全鏈路解析)
做太陽能板監(jiān)控Solar Panel Monitor這個項目起因其實很樸素作為IoT愛好者家里裝了兩塊光伏板之后我特別想知道每天到底發(fā)了多少電、什么時候發(fā)電效率最高、要不要擦板子。市面上的成品監(jiān)控要么綁定逆變器品牌要么只能看到電表數(shù)據(jù)始終沒法看到板子層面的實時電壓、電流和溫度。于是就有了這套完全自建的IoT監(jiān)控方案前后花了大概兩周硬件成本不到200元卻把光伏系統(tǒng)從“黑盒”變成了“可視化設(shè)備”。這篇文章我把完整鏈路拆開講從硬件選型、接線校準(zhǔn)到固件、數(shù)據(jù)管線和實際分析結(jié)果適合有小塊太陽能板或者想練手IoT采集的同學(xué)參考。1. 先說說為什么要給自己的光伏板配一套IoT監(jiān)測1.1 沒裝監(jiān)控之前我到底錯過了什么大多數(shù)家用光伏系統(tǒng)的用戶日常能看到的只有逆變器液晶屏上的累計電量和瞬時功率。頂部瞬時功率是一個數(shù)字無法反映一天內(nèi)變化的曲線。除非每天同一時間記錄否則很難判斷板子是否被樹葉遮擋、是否布滿灰塵、背板溫度是否過高。更麻煩的是當(dāng)發(fā)電量下降時故障可能已經(jīng)持續(xù)幾天甚至幾周等到電費單出來才發(fā)現(xiàn)。損耗的電量無法補救。我最初安裝的是12V小功率離網(wǎng)系統(tǒng)板子輸出直接進MPPT控制器給蓄電池充電控制器帶一個簡易屏幕能看到電壓電流但這些數(shù)據(jù)不聯(lián)網(wǎng)也沒有歷史記錄。某天我發(fā)現(xiàn)充電電流比平時低了30%排查好久才發(fā)現(xiàn)是連接器內(nèi)部氧化。如果當(dāng)時有持續(xù)監(jiān)控這個問題當(dāng)天就能發(fā)現(xiàn)。等到自己動手做這套IoT監(jiān)控后我再也沒有靠“感覺”維護過光伏系統(tǒng)。1.2 這個系統(tǒng)最終能監(jiān)測什么我設(shè)計的這套監(jiān)控指標(biāo)如下表監(jiān)測項傳感器/方式量程/精度說明組件輸出電壓INA2260-36V精度±0.1%直接讀Bus Voltage組件輸出電流INA2260-2A或0-20A取決于分流電阻電流通過分流電阻換算實時功率軟件計算由電壓×電流得到單位W背板溫度DS18B20防水探頭-55~125°C精度±0.5°C貼在背板中心附近環(huán)境溫度/濕度SHT30溫度±0.3°C濕度±2%RH用于與背板溫度對比累計發(fā)電量數(shù)據(jù)庫聚合對功率做時間積分單位kWh設(shè)備在線狀態(tài)MQTT Last Will實時掉線自動告警采樣頻率這里要特別說明傳感器采集頻率不等于上報頻率。我建議設(shè)備端內(nèi)部以1秒間隔做中值濾波然后每5秒通過MQTT上報一次如果不需要精細曲線1分鐘一次也可以。存儲方面時序數(shù)據(jù)庫里保留原始數(shù)據(jù)Grafana展示時按時間聚合。如果你后面還想做輻照度、風(fēng)速、傾角等擴展項I2C總線上還能繼續(xù)掛傳感器整體架構(gòu)不用推倒重來。1.3 誰會需要這樣一套東西如果你只有一塊小板子給手機充電可能用不上。但如果你有完整的離網(wǎng)電源系統(tǒng)、屋頂光伏陣列或者正在做便攜電站這套監(jiān)控能幫你回答很多實際問題板子朝向換一下發(fā)電量差多少、梅雨季節(jié)要不要清洗、電池充滿后怎么主動降低充電電流。而且它本身也是一個極好的IoT教學(xué)項目覆蓋了傳感器采集、無線通信、服務(wù)端數(shù)據(jù)管道和可視化告警幾乎把常見物聯(lián)網(wǎng)場景全串起來了。我后來做工業(yè)數(shù)據(jù)采集項目時很多思路都是從這個小項目里遷移過去的。2. 硬件選型與總體架構(gòu)我為什么選了ESP32 INA2262.1 系統(tǒng)分層設(shè)計這個監(jiān)控系統(tǒng)的架構(gòu)可以分為三層。感知層由電流電壓采樣、溫度探頭組成負責(zé)把物理量變成電信號邊緣層用ESP32完成數(shù)據(jù)讀取、濾波、協(xié)議轉(zhuǎn)換和上報應(yīng)用層跑在局域網(wǎng)內(nèi)的一臺小服務(wù)器上包含MQTT Broker、時序數(shù)據(jù)庫和可視化看板。之所以沒有選“設(shè)備直接上云”的方案是因為很多光伏系統(tǒng)部署在沒有外網(wǎng)的環(huán)境而且數(shù)據(jù)留在本地更安全后期接Home Assistant也方便。這種分層設(shè)計的好處是每一層都能獨立替換。傳感器壞了只換傳感器MQTT服務(wù)掛了不影響采集端數(shù)據(jù)庫想從InfluxDB遷到TimescaleDB也不改動設(shè)備固件。后面的選型邏輯都圍繞“穩(wěn)定、低成本、好維護”這三個關(guān)鍵詞展開適合個人項目也適合小規(guī)模的邊緣采集場景。2.2 主控ESP32是個人項目里的性價比之王主控的選擇其實糾結(jié)過一段時間。Arduino Uno很容易上手但需要外接ESP8266等模塊才能聯(lián)網(wǎng)而且Flash小做個JSON解析都勉強樹莓派性能強可以直接跑Python甚至數(shù)據(jù)庫但成本高、啟動慢、功耗大放在戶外盒子里還要擔(dān)心SD卡損壞。STM32性能不錯但面對WiFi協(xié)議棧開發(fā)門檻明顯更高。ESP32幾乎是為這種場景設(shè)計的。雙核240MHz跑WiFi和傳感器采集不會互相拖累內(nèi)置ADC、I2C、SPI、UART支持Arduino框架、ESP-IDF、MicroPython價格在20元左右模塊拆壞了也不心疼。實際測試下來它同時處理INA226輪詢、DS18B20讀取和MQTT發(fā)布CPU占用率很低還有余量做本地平均濾波和異常檢測。主控聯(lián)網(wǎng)方式開發(fā)成本優(yōu)點缺點Arduino Uno需外接ESP8266/ENC28J60低入門簡單Flash小內(nèi)存少協(xié)議棧弱ESP32內(nèi)置WiFi/BLE中低雙核、外設(shè)豐富、成本低ADC精度一般Raspberry Pi有線/WiFi高可跑服務(wù)端成本高、功耗大、啟動慢STM32WiFi模塊外掛模塊高穩(wěn)定、性能強開發(fā)周期長我的結(jié)論很明確如果是單點采集、數(shù)據(jù)量不大ESP32是最合適的。等將來節(jié)點多了再考慮換成帶以太網(wǎng)的高性能網(wǎng)關(guān)。2.3 電流電壓采樣INA226比純ADC方案靠譜在哪很多人第一步會想直接用ESP32的ADC讀分壓電阻測電壓再配一個霍爾電流傳感器。這個方案不是不行但誤差會讓你懷疑人生。ESP32自帶的ADC在中等電壓范圍內(nèi)線性度一般而且參考電壓會隨溫度變化分壓電阻的精度和溫漂也會貢獻幾個百分點的誤差。對于太陽能板這種波動本來就很大的電源最后數(shù)據(jù)只能看個趨勢沒法作為決策依據(jù)。INA226是一顆I2C接口的16位電流/電壓監(jiān)控芯片常見模塊價格十幾塊錢。它內(nèi)部帶ADC能同時測母線電壓VBUS和分流電阻兩端電壓VSHUNT通過校準(zhǔn)寄存器配置好分流電阻阻值和預(yù)期量程后可以直接讀出電流、電壓還能額外算功率。最關(guān)鍵的一點是它的測量鏈路不經(jīng)過MCU的ADC噪聲和誤差都小得多。如果你對精度有要求我強烈建議用INA226而不是ACS712。ACS712是霍爾型適合隔離大電流但零電流輸出不是0溫度漂移明顯小電流下誤差很大。太陽能板監(jiān)控屬于小功率直流場景用INA226串聯(lián)采樣更合適。選模塊時要注意兩個參數(shù)一是模塊上的分流電阻阻值常見的有0.1Ω、0.002Ω、0.01Ω幾種。0.1Ω適合小電流如0-2A壓降大但分辨率高0.002Ω適合大電流如0-50A發(fā)熱小但微弱電流測不準(zhǔn)。二是芯片的共模電壓范圍。INA226的VBUS最高能到36V也有60V版本如果你的太陽能板開路電壓超過這個值就要用分壓電阻或者換更高耐壓的芯片。2.4 背板溫度和環(huán)境溫濕度傳感器背板溫度我用了一根DS18B20防水探頭直接貼在太陽能板背面鋁邊框和背板交接處。DS18B20是單總線數(shù)字傳感器三根線就能搞定多個探頭還能掛到同一條總線上每個探頭有唯一64位地址。需要注意的是貼合時要涂抹導(dǎo)熱硅脂不然測的是探頭旁邊空氣的溫度不是板子溫度。環(huán)境溫濕度我選的是SHT30因為DHT11/DHT22精度太差讀取時序又嚴格換SHT30后讀數(shù)穩(wěn)定多了價格也才幾塊錢。這兩個傳感器都接在ESP32的GPIO上I2C共用一個總線單總線單獨一個引腳。如果以后想加輻照度傳感器輻照計I2C總線上還能繼續(xù)掛。3. 接線、校準(zhǔn)與防坑這步最容易被低估3.1 接線拓撲與電源方案接線是整個項目里最容易出問題的一步。我先說總體結(jié)構(gòu)太陽能板正極輸出先接到一個直流保險絲然后進入INA226模塊的采樣回路再從模塊出來接到MPPT控制器或負載的輸入端。也就是說電流采樣電阻串聯(lián)在太陽能板到控制器之間的正極線上。同時模塊需要供電我直接用了5V的獨立電源USB充電頭給ESP32和傳感器供電INA226模塊本身用I2C的3.3V供電但它的采樣輸入是與供電隔離的所以采樣電壓可以達到幾十伏。這里有個坑如果你用同一個電源給ESP32供電同時太陽能板又通過控制器給同一個電池充電地線需要共地。否則INA226讀出的電壓和電流會出現(xiàn)跳變或漂移。我用的是獨立USB電源地和太陽能板負極共地這樣就避免了浮動電壓問題。實際操作時建議先用萬用表確認太陽能板正負極然后先接控制器的電池端再接太陽能板避免控制器在無電池狀態(tài)下空載。保險絲和直流斷路器不能省。太陽能板只有在短路時才會有大電流但一旦接線錯誤可能瞬間燒毀控制器和模塊。我在正極線上串了一個額定電流1.5倍于板子Isc的保險絲又在模塊輸入端并聯(lián)了一個TVS管用來吸收開關(guān)瞬態(tài)高壓。這些保護元件加起來不到十塊錢但能避免很多“莫名其妙”的損壞。3.2 校準(zhǔn)流程不校準(zhǔn)的INA226數(shù)據(jù)只能當(dāng)參考INA226雖然精度高但出廠默認校準(zhǔn)寄存器并不能直接用于任意分流電阻。拿到模塊后必須先做兩件事確認分流電阻阻值以及寫入正確的校準(zhǔn)值。如果模塊只印著0.1Ω沒有給出精確阻值就用萬用表電阻檔實測。隨后用官方INA226計算表出一個校準(zhǔn)值。以0.1Ω分流電阻、預(yù)期最大電流2A為例CurrentLSB 2A / 32768 ≈ 0.000061A/bit取整到0.0001A/bit對應(yīng)Cal 0.00512 / (0.0001 × 0.1) 512。寫入Cal寄存器后電流寄存器LSB為0.1mA讀取值就是電流。電壓寄存器不需要校準(zhǔn)它是絕對值測量但要注意VBUS寄存器是14位LSB為1.25mV直接乘1.25就是毫伏。校準(zhǔn)的作用是讓芯片內(nèi)部的計算結(jié)果和真實電流一致。如果你不寫Cal寄存器電流讀值大概率是錯的。另一個坑是零點偏移。芯片本身有offset系統(tǒng)在空載時可能讀到幾十mA的電流。我在軟件里加入了開機空載校準(zhǔn)設(shè)備啟動后讀取10次電流取平均作為之后每幀數(shù)據(jù)的減數(shù)。這樣即使分流電阻有輕微溫漂也能通過定期校準(zhǔn)減小影響。要得到進一步精確的校準(zhǔn)可以用一塊高精度電子負載或一個已知阻值的功率電阻通入固定電流然后調(diào)整Cal值直到讀數(shù)匹配。不要用電機這類波動負載校準(zhǔn)。校準(zhǔn)完成后把參數(shù)寫到EEPROM或者代碼的配置頭文件里方便下次刷機恢復(fù)。3.3 常見接線錯誤和癥狀列幾個我實際遇到的癥狀和原因電壓讀數(shù)正常電流一直是0大概率分流電阻兩端的采樣線接反了或者INA226模塊的V-端沒有串進主回路。電壓、電流讀數(shù)來回跳采樣線接觸不良或地線沒有共地。設(shè)備一接太陽能板就重啟太陽能板在強光下的瞬時電壓超過了供電電路的耐壓或共地噪聲耦合到ESP32的復(fù)位引腳。溫度讀數(shù)明顯偏高DS18B20防水探頭沒貼緊背板被太陽曬到了。排查這些問題時最好先用實驗室直流電源代替太陽能板設(shè)置一個固定的12V電壓和1A電流限制確認采集端讀數(shù)無誤后再接真實板子。這樣可以把電源問題與通信問題隔離。4. 固件邏輯從采集到上報的完整鏈路4.1 采集流程設(shè)計固件的任務(wù)可以拆成四塊初始化、循環(huán)采集、組包上報、異常處理。系統(tǒng)上電后先初始化Wire、INA226、DS18B20、DHT30再連WiFi和MQTT。如果WiFi一直連不上我采用“不阻塞啟動”策略先開始本地采集后臺異步重連而不是卡死在connect函數(shù)里。這樣即使網(wǎng)絡(luò)壞了設(shè)備仍然在本地運行重連后能拿到接近實時的數(shù)據(jù)。設(shè)備端緩存最近的上報值Mqtt連接建立后先補發(fā)一幀狀態(tài)。采集循環(huán)里我以1秒為周期讀取全部傳感器。電壓、電流各讀10次去掉最大最小值后取平均把平均值存進一個環(huán)形緩沖。每5秒計算一次5秒窗口的移動平均作為上報數(shù)據(jù)。移動平均的作用是濾掉太陽能板由于云層遮擋或開關(guān)電源引起的毛刺避免Grafana上的曲線變成鋸齒。4.2 關(guān)鍵代碼段說明我用Arduino框架開發(fā)核心代碼大概如下。INA226庫選用Rob Tillaart的INA226庫MQTT用PubSubClient。代碼邏輯很直接需要注意的點我寫在注釋里。#include Wire.h #include INA226.h #include WiFi.h #include PubSubClient.h #include ArduinoJson.h INA226 ina(Wire, 0x40); // 默認地址 I2C 0x40 const float shuntResistor 0.1f; // 根據(jù)模塊實際測量寫入 float currentLSB 0.0001f; // 例子最大2A / 32768 ≈ 0.000061取0.0001 uint16_t calValue (uint16_t)(0.00512f / (currentLSB * shuntResistor)); void setup() { Wire.begin(21, 22); // ESP32 I2C引腳 if (!ina.begin()) { Serial.println(INA226 not found); } ina.setCalibration(calValue); // 其他傳感器初始化... }上面的calValue計算出來是512對于0.1Ω和0.0001A/bit正好是512。如果你換用0.01Ω和2A量程Cal值就會變成5120。這個值不能亂填否則電流寄存器讀出來的數(shù)和真實電流差很多倍。讀取一幀數(shù)據(jù)并組包的部分我貼一個片段String buildPayload() { float v ina.getBusVoltage(); // 單位V float i ina.getCurrent(); // 單位A經(jīng)過Cal寄存器換算后 float p v * i; float temp readDs18b20(); GlobalJsonDoc.clear(); JsonObject obj GlobalJsonDoc.toJsonObject(); obj[device] solar_panel_1; obj[v] serialized(String(v, 3)); obj[i] serialized(String(i, 3)); obj[p] serialized(String(p, 3)); obj[t] serialized(String(temp, 1)); // 時間戳由MQTT broker側(cè)或數(shù)據(jù)庫側(cè)補充設(shè)備端不依賴NTP String out; GlobalJsonDoc.serializeTo(out); return out; }這里有個小設(shè)計設(shè)備端不在payload里寫ts字段而是讓服務(wù)端收到消息時按當(dāng)前時間打標(biāo)。原因是ESP32在斷網(wǎng)后系統(tǒng)時間會不準(zhǔn)如果以設(shè)備時間為準(zhǔn)恢復(fù)網(wǎng)絡(luò)前的時間戳就錯亂了。如果一定要設(shè)備端時間戳就要通過NTP同步并定期校準(zhǔn)。我用ArduinoJson庫而不是手動拼字符串主要是為了避免特殊字符轉(zhuǎn)義和浮點數(shù)格式問題。ESP32的Flash足夠放這份JSON庫序列化速度也很快。發(fā)布消息時把retained標(biāo)志設(shè)成false避免舊數(shù)據(jù)被新訂閱者當(dāng)成最新狀態(tài)。4.3 上報頻率、QoS與數(shù)據(jù)量估算太陽能板的電氣特征變化不會特別劇烈1秒采集、5秒發(fā)布已經(jīng)足夠。如果發(fā)布太頻繁ESP32和MQTT broker都會白白耗電而且長時間運行會積攢大量碎片數(shù)據(jù)。每秒發(fā)布一個JSON一天就是86400條雖然InfluxDB能扛住但查詢和告警沒必要。我的經(jīng)驗是采集1秒、發(fā)布5秒保留原始數(shù)據(jù)一個月Grafana按1分鐘聚合展示。如果后續(xù)需要做更多分析可以用InfluxDB的連續(xù)查詢把數(shù)據(jù)降采樣到1分鐘明細再保留更長時間。MQTT QoS一般選1。QoS 0可能丟失QoS 2會引入兩輪握手對5秒一條的物聯(lián)網(wǎng)數(shù)據(jù)來說是浪費。Topic的設(shè)計也要注意我用的是sensor/solar_panel_1/data如果以后有多個板子可以直接擴展sensor/solar_panel_2/data。服務(wù)端訂閱通配符sensor//data就能收到全部數(shù)據(jù)。一條典型的MQTT消息長這樣{device:solar_panel_1,v:30.123,i:1.356,p:40.84,t:42.5}在InfluxDB里存下后查詢時按設(shè)備標(biāo)簽和字段篩選很方便。如果未來想加入多臺板子只需要改固件里的device字段服務(wù)端不用動。4.4 斷線重連與看門狗WiFi在任何戶外環(huán)境都不可能永遠穩(wěn)定。我遇到最典型的情況是路由器重啟后ESP32的WiFi庫會自動重連但MQTT連接并不會自動恢復(fù)必須監(jiān)聽MqttClient.connected()并在斷開時主動調(diào)用reconnect()。在重試邏輯里加上隨機退避比如每次重試間隔增加100-500ms避免所有設(shè)備同時重連瞬間把broker打掛。同時啟用ESP32的Task Watchdog把采集主循環(huán)喂狗放在loop()開頭。如果一個I2C讀取卡死超過5秒看門狗就強制重啟。這種方式看起來粗暴但非常有效能避免無人值守設(shè)備“假死”一星期。另外我把上次上報時間存在RTC內(nèi)存里重啟后如果發(fā)現(xiàn)距離上次上報超過閾值會立刻補發(fā)一幀方便服務(wù)端做在線狀態(tài)判斷。5. 數(shù)據(jù)落地MQTT、InfluxDB、Grafana三件套怎么串5.1 為什么選Mosquitto InfluxDB Grafana服務(wù)端我一開始打算用Node-RED直接收MQTT再寫數(shù)據(jù)庫后來發(fā)現(xiàn)對純監(jiān)控場景來說Node-RED有點重。改用Telegraf訂閱MQTT直接寫InfluxDB配合Grafana看板整個鏈路就是三個獨立小服務(wù)哪個掛了都容易單獨處理。Mosquitto是Eclipse社區(qū)的老牌MQTT Broker資源占用小配置文件簡單Linux上一條命令就能裝。InfluxDB是時序數(shù)據(jù)庫專門處理這種“每隔幾秒一條帶時間戳的數(shù)據(jù)”。Grafana則負責(zé)把數(shù)據(jù)變成圖表和告警。這三件套在物聯(lián)網(wǎng)監(jiān)控圈幾乎成了默認組合我遇到問題的時候搜資料也特別方便。如果你愿意折騰也可以把InfluxDB換成