天文用攜帶型天氣站 製作日期:2021/06/16 (三) ~ 2021/06/22 (二)
基本介紹 對於天文觀測或是天文攝影,一個光害少的好地點是必須的。那麼我們要怎麼判斷一個地方的光害汙染程度呢,這時候科學家定義了新的標準,來衡量天空背景亮度的強度。而目前業餘常見的測量儀器是加拿大公司 Unihedron 出的Sky Quality Meter。
這種手持式的儀器在業餘天文界算是常見,操作方式很簡單,僅需要對準天空,裡面的感測器就會測量光強度,並換算成SQM數值,夜空的數值一般介於 16~22 ,數字越大代表光害越小,可以用來對不同地方的光汙染進行比較。
此外,天文攝影很注重環境的溫溼度,相機在不同溫度下自然產生的熱雜訊的強度會不同,需要記錄環境溫度,後期才能根據該溫度拍攝對應的校正檔,來修正我們所得到的資料。而濕度可以很大程度的影響我們拍攝,當濕度過高時望遠鏡很容易結露,對鏡面和相機傷害較大,因此如果能觀測環境濕度也能幫助我們在濕度過高的情況下及時保護儀器。
但是但是,市面上同時結合SQM和溫溼度的商品非常昂貴(NT 5000+) ,而且幾乎找不到,而這種看似不起眼的攜帶型天氣站卻對天文觀測非常重要,因此我打算做一個基於ESP32的天文用攜帶型天氣站,並利用ESP32的網路功能,結合第三方網站達到隨時用手機或電腦就能監控的功能,還能一併將數據紀錄起來。
以下材料是我這次有用到的,材料約一千出頭。
品項
價格
NodeMCU-32S 開發板
360
BME280 溫溼度氣壓感測器
210
TSL2591 環境光感測器
290
1.8吋 TFT螢幕
210
開發板及感測器介紹 NodeMCU-32S 這次我購買的是AI-thinker的NodeMCU-32S v1.3,聽說相比其他的ESP32開發板,這款在Wifi連線方面十分穩定,不會出現斷線等問題。
NodeMCU-32S是基於ESP32並且內建Wifi及藍芽晶片的開發板,可以兼容Arduino的感測器及語法,各項功能也比Arduino Uno強,針腳高達38個,並且NodeMCU-32S的Vin支持5V輸出,可以用來推動3.3V推不動的裝置。
Arduino Uno
ESP8266
ESP32
MCU
ATMega328P
Tensilica Xtensa Lx106
Tensilica Xtensa LX6
核心
單核心 20MHz
單核心 80/160MHz
雙核心 160/240MHz
SRAM
16KB
160KB
512KB
Flash空間
32KB
1-4MB
4-32MB
GPIO
13
8
18
ADC
8
1
18
PWM
6
8
16
類比解析度
1024
1024
4096
I2C
1組
1組
2組
SPI
1組
1組
3組
Wifi
無
802.11 b/g/n
802.11 b/g/n
Bluetooth
無
無
BLE 4.2
內建溫度感測
無
無
有
內建霍爾感測器
無
無
有
內建觸控電容
無
無
10組
售價
100-200
100-200
200-300
NodeMCU-32S 通訊腳位
SPI
MOSI
MISO
CLK
CS
HSPI
GPIO13
GPIO12
GPIO14
GPIO15
VSPI
GPIO23
GPIO19
GPIO18
GPIO5
I2C
SDA
SCL
I2C
GPIO21
GPIO22
只能作為輸入的GPIO為GPIO34, GPIO35, GPIO36, GPIO39, 其他針腳都可以雙向輸入輸出。
BME280 溫溼度氣壓感測器 BME280相比常見的DHT11, DHT22,有更高的精度,而且它也內建了氣壓感測器,可以用來量測高度及環境氣壓,對於天氣站來說相當適合。市面上的BME280有分3.3V和5V兩種,我購買的是3.3V版本,這點要特別注意,以免將感測器燒掉。另外買來需要將針腳焊接上去。
買來發現它的尺寸真的超級小,看來很省空間
DHT11
DHT12
BME280
感測功能
溫度,濕度
溫度,濕度
溫度,濕度,氣壓
溫度範圍
0~50ºC
-40~80ºC
-40~85ºC
溫度精度/解析度
+-2ºC/1°C
+-0.5ºC/0.1ºC
+-0.5ºC/0.01°C
濕度範圍
20~90%
0~100%
0~100%
濕度精度/解析度
+-5%/1%
+-5%/0.1%
+-3%/0.008%
氣壓範圍
無
無
300-1100hPa
氣壓精度/解析度
無
無
+-1hPa/0.18hPa
搜尋bme280來安裝函式庫,有很多種版本,我安裝的是Adafruit的。
腳位
NodeMCU-32S
Vcc
3.3V
GND
GND
SCL
GPIO18
SDA(MOSI)
GPIO23
CSE
GPIO5
SDO(MISO)
GPIO19
這款可以用I2C或是SPI通訊,我選擇用I2C通訊,因此只需要接SDA和SCL,我將SCL接到GPIO22,SDA接到GPIO21。接下來使用下面的測試程式碼,我們使用I2C所以不用更改,若使用SPI則須將注釋掉的地方做修改。高度計功能則需要依賴設定平地氣壓為前提去計算,因此這裡設定一大氣壓(1013.25百帕)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <Wire.h> #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> #define SEALEVELPRESSURE_HPA (1013.25) Adafruit_BME280 bme; unsigned long delayTime;void setup () { Serial.begin(9600 ); Serial.println(F("BME280 test" )); bool status; status = bme.begin(0x76 ); if (!status) { Serial.println("Could not find a valid BME280 sensor, check wiring!" ); while (1 ); } Serial.println("-- Default Test --" ); delayTime = 1000 ; Serial.println(); } void loop () { printValues(); delay(delayTime); } void printValues () { Serial.print("Temperature = " ); Serial.print(bme.readTemperature()); Serial.println(" *C" ); Serial.print("Pressure = " ); Serial.print(bme.readPressure() / 100.0F ); Serial.println(" hPa" ); Serial.print("Approx. Altitude = " ); Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA)); Serial.println(" m" ); Serial.print("Humidity = " ); Serial.print(bme.readHumidity()); Serial.println(" %" ); Serial.println(); }
將baud設定成9600,你應該可以看到感測器輸出的數據了
更多詳細資料:https://randomnerdtutorials.com/esp32-web-server-with-bme280-mini-weather-station/
TSL2591 環境光感測器 TSL2591是一個超高靈敏度的光感測器,從 188 μlux 到 88000 lux 都能量測,動態範圍相當廣。支持3.3V~5V輸入,使用I2C通訊。另外它能夠量測黑暗環境下的光強度,可以應用在測量SQM(Sky Quality Meter)上,所以被我挑選當光感測器。挑選時要注意大部分的光感測器無法測量到 0.1 lux 以下的光照度,對於測量暗空品質而言,至少要到能量到 0.0003 lux 的靈敏度。
環境
照度(lux)
烈日
100000
陰天
500~6000
室內
300
路燈
5
滿月
0.2
星空
0.0003
先安裝TSL2591的函式庫,我用的是Adafruit的。
針腳
NodeMCU-32S
Vin
3.3V/5V
GND
GND
3vo
輸出3.3V(100mA max)
Int
INTerrupt pin(暫時用不到)
SDA
GPIO21
SCL
GPIO22
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 #include <Wire.h> #include <Adafruit_Sensor.h> #include "Adafruit_TSL2591.h" Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591 ); void configureSensor (void ) { tsl.setGain(TSL2591_GAIN_MED); tsl.setTiming(TSL2591_INTEGRATIONTIME_300MS); Serial.println(F("------------------------------------" )); Serial.print (F("Gain: " )); tsl2591Gain_t gain = tsl.getGain(); switch (gain) { case TSL2591_GAIN_LOW: Serial.println(F("1x (Low)" )); break ; case TSL2591_GAIN_MED: Serial.println(F("25x (Medium)" )); break ; case TSL2591_GAIN_HIGH: Serial.println(F("428x (High)" )); break ; case TSL2591_GAIN_MAX: Serial.println(F("9876x (Max)" )); break ; } Serial.print (F("Timing: " )); Serial.print((tsl.getTiming() + 1 ) * 100 , DEC); Serial.println(F(" ms" )); Serial.println(F("------------------------------------" )); Serial.println(F("" )); } void setup (void ) { Serial.begin(9600 ); Serial.println(F("Starting Adafruit TSL2591 Test!" )); if (tsl.begin()) { Serial.println(F("Found a TSL2591 sensor" )); } else { Serial.println(F("No sensor found ... check your wiring?" )); while (1 ); } configureSensor(); } void simpleRead (void ) { uint16_t x = tsl.getLuminosity(TSL2591_VISIBLE); Serial.print(F("[ " )); Serial.print(millis()); Serial.print(F(" ms ] " )); Serial.print(F("Luminosity: " )); Serial.println(x, DEC); } void advancedRead (void ) { uint32_t lum = tsl.getFullLuminosity(); uint16_t ir, full; ir = lum >> 16 ; full = lum & 0xFFFF ; Serial.print(F("[ " )); Serial.print(millis()); Serial.print(F(" ms ] " )); Serial.print(F("IR: " )); Serial.print(ir); Serial.print(F(" " )); Serial.print(F("Full: " )); Serial.print(full); Serial.print(F(" " )); Serial.print(F("Visible: " )); Serial.print(full - ir); Serial.print(F(" " )); Serial.print(F("Lux: " )); Serial.println(tsl.calculateLux(full, ir), 6 ); } void unifiedSensorAPIRead (void ) { sensors_event_t event; tsl.getEvent(&event); Serial.print(F("[ " )); Serial.print(event.timestamp); Serial.print(F(" ms ] " )); if ((event.light == 0 ) | (event.light > 4294966000.0 ) | (event.light <-4294966000.0 )) { Serial.println(F("Invalid data (adjust gain or timing)" )); } else { Serial.print(event.light); Serial.println(F(" lux" )); } } void loop (void ) { advancedRead(); delay(500 ); }
上傳後就能看到感測器輸出的訊息了,這一款可以感測紅外光(IR)和紅外線+可見光(IR+Visible)的光強度,相減可以得到可見光的光強度,能應用在不同地方。
從程式碼可以看到,這個感測器可以設定Gain,若要量測高亮度的光源,要將Gain調成Low,如果是要用在黑暗環境下,要將Gain調成High。另外還有Integration Time,時間調越長對偵測黯淡的光會有幫助,但是相對應的感測頻率會降低。
1.8吋TFT螢幕 這片螢幕的解析度為128x160,透過SPI串口控制,支援5V及3.3V供電,還內建SD卡擴展電路,提供讀寫SD卡的能力,驅動IC是ST7735。經過搜尋這塊螢幕似乎有兩個版本,8pin(SD)和10pin(micro SD)的版本,10pin多出來的是兩個NC針腳,並不需要接線(NC代表Not Connect,不與晶片連結,即空腳),剩下的8pin跟8pin版本的功能一樣。另外有一側4pin接口是給SD卡用的,板子上印有SD開頭字樣。
pin
Arduino
NodeMCU-32S (ai-thinker)
Vcc
3.3V/5V
3.3V/5V
GND
GND
GND
CLK(SCL,SCK)
PIN13
GPIO18
SDA(MOSI)
PIN11
GPIO23
RS(A0,DC)
PIN8
任意(這裡選GPIO17)
RST
PIN9
EN
CS(SS)
PIN10
GPIO5
NodeMCU有三組SPI接口,板子命名為SPI,HSPI和VSPI,之間並沒有差別,都是SPI接口,僅是區分不同組而已。
SPI介紹: https://ithelp.ithome.com.tw/articles/10245910
使用前要先安裝Adafruit GFX函式庫,點選Librabies Manager,搜尋ST7735就能找到了。點選安裝就可以了,沒意外它會連GFX函式庫也一起安裝,沒有的話就搜尋一下安裝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <Adafruit_GFX.h> #include <Adafruit_ST7735.h> #include <Fonts/FreeSerif12pt7b.h> #include <Fonts/FreeSansBold9pt7b.h> #include <Fonts/FreeSans9pt7b.h> #include <SPI.h> #define TFT_CS 5 #define TFT_DC 17 #define TFT_RST 0 Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); void setup (void ) { tft.initR(INITR_BLACKTAB); tft.setRotation(1 ); tft.fillScreen(ST77XX_BLACK); tft.setCursor(10 , 30 ); tft.setTextColor(ST77XX_WHITE); tft.setFont(&FreeSerif12pt7b); tft.print("Hello World !" ); tft.setCursor(0 , 60 ); tft.setTextColor(ST77XX_RED); tft.setFont(&FreeSansBold9pt7b); tft.print("I'm red !" ); tft.setCursor(0 , 80 ); tft.setTextColor(ST77XX_YELLOW); tft.setFont(&FreeSans9pt7b); tft.print("I'm yellow" ); showIcons(); } void showIcons () { tft.fillRoundRect(30 , 95 , 30 , 30 , 5 , ST77XX_MAGENTA); tft.fillCircle(80 , 110 , 15 , ST77XX_BLUE); tft.drawRect(105 , 95 , 30 , 30 ,ST77XX_GREEN); delay(500 ); } void loop () { }
顯示效果,背光感覺蠻亮的,下次應該換成OLED試試看。
可以透過增加在Vcc前加一顆100歐姆電阻來稍微降低螢幕亮度,可惜這塊螢幕的LED和驅動晶片供電不是分開的,因此不能單獨去控制LED的亮度,若調降太多電壓,螢幕會開不起來。
接著我改寫了一下代碼,透過控制螢幕的Vcc腳位,每隔十秒開關螢幕一次,這樣的功能搭配按鈕就能達成螢幕開關的功能,當需要的時候再打開螢幕,可以省不少電。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <Adafruit_GFX.h> #include <Adafruit_ST7735.h> #include <Fonts/FreeSerif12pt7b.h> #include <Fonts/FreeSansBold9pt7b.h> #include <Fonts/FreeSans9pt7b.h> #include <SPI.h> #define TFT_CS 5 #define TFT_DC 17 #define TFT_RST 0 #define TFT_Vcc 16 Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); int screen_switch = 1 ;void setup (void ) { pinMode(TFT_Vcc, OUTPUT); digitalWrite(TFT_Vcc, screen_switch); tftInit(); } void tftInit () { tft.initR(INITR_BLACKTAB); tft.setRotation(1 ); tft.fillScreen(ST77XX_BLACK); tft.setCursor(10 , 30 ); tft.setTextColor(ST77XX_WHITE); tft.setFont(&FreeSerif12pt7b); tft.print("Hello World !" ); tft.setCursor(0 , 60 ); tft.setTextColor(ST77XX_RED); tft.setFont(&FreeSansBold9pt7b); tft.print("I'm red !" ); tft.setCursor(0 , 80 ); tft.setTextColor(ST77XX_YELLOW); tft.setFont(&FreeSans9pt7b); tft.print("I'm yellow" ); delay(500 ); } void loop () { screen_switch = 1 - screen_switch; digitalWrite(TFT_Vcc, screen_switch); tftInit(); delay(10000 ); }
Adafruit GFX 函式庫介紹: https://atceiling.blogspot.com/2019/09/arduino65adafruitgfx_7.html
安裝及配置開發環境 我這次使用的開發版是NodeMCU-32S,可以用Arduino IDE進行開發。因此我這次使用的是Arduino IDE 2.0,2.0版本目前還在beta測試階段,但是相比舊版的IDE,不僅編譯速度變快,同時也加入許多方便的功能。IDE可以從官網 下載。
點選左上角的File->Preferences,在最下面一欄添加URL的地方,將下方網址添加給IDE。https://dl.espressif.com/dl/package_esp32_index.json
輸入後點選左側欄位的Board Manager,輸入esp32就可以安裝相關套件了。
待電腦安裝完畢,點選上方的no board selected,搜尋nodemcu就能找到NodeMCU-32S的選項,這樣一來就能進行NodeMCU-32S的開發了。
連接開發板後卻發現找不到裝置,這是因為我們還沒安裝驅動,我購買的板子它的USB晶片是CH340C,它的驅動可以在這裡 找到,下載後安裝就能辨識裝置了,開啟裝置管理員可以看到正確識別。
重新啟動IDE,就能找到板子了。再來我們寫個簡單程序測試一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <Arduino.h> #define LED 2 void setup () { Serial.begin(115200 ); pinMode(LED, OUTPUT); } void loop () { digitalWrite(LED, HIGH); Serial.println("LED is on" ); delay(1000 ); digitalWrite(LED, LOW); Serial.println("LED is off" ); delay(1000 ); }
編譯簡單程序卻報錯,Error: 2 UNKNOWN: exec: “cmd”: executable file not found in %PATH%,解決方式是將:\Windows\System32路徑加到環境變量裡,這個資料夾是cmd.exe的所在位置。添加完後重啟電腦。如果不想重啟,用管理員身分開啟cmd,輸入以下指令1 2 taskkill /f /im explorer.exe explorer.exe
輸入完後關閉cmd即可更新環境變數,重啟IDE就能抓到cmd了。
上傳後板子上的LED每格一秒會閃爍一次,並在Serial Monitor可以看到輸出的訊息。
撰寫天氣站軟體 基本功能(結合BME280, TSL2591) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <Wire.h> #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> #include <Adafruit_TSL2591.h> #define SEALEVELPRESSURE_HPA (1001.25) #define GAIN TSL2591_GAIN_MED #define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_300MS #define DELAY_TIME 10000 Adafruit_BME280 bme; Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591 ); float temp, humidity, pressure, altitude, luminosity;void setup () { Serial.begin(9600 ); init_bme280(); init_tsl2591(); } void loop () { temp = get_temperature(); humidity = get_humidity(); pressure = get_pressure(); altitude = get_altitude(); luminosity = get_luminosity(); Serial.print("Temp: " ); Serial.print(temp); Serial.println("*C" ); Serial.print("Humid: " ); Serial.print(humidity); Serial.println("%" ); Serial.print("Pressure: " ); Serial.print(pressure/100.0 ); Serial.println("hPa" ); Serial.print("Alt: " ); Serial.print(altitude); Serial.println("m" ); Serial.print("Lumin: " ); Serial.print(luminosity); Serial.println("lux" ); Serial.println("" ); delay(DELAY_TIME); } void init_bme280 () { bme.begin(0x76 ); } void init_tsl2591 () { tsl.begin(0x29 ); tsl.setGain(GAIN); tsl.setTiming(INTEGRATIONTIME); } float get_temperature () { return bme.readTemperature(); } float get_humidity () { return bme.readHumidity(); } float get_altitude () { return bme.readAltitude(SEALEVELPRESSURE_HPA); } float get_pressure () { return bme.readPressure(); } float get_luminosity () { uint32_t lum = tsl.getFullLuminosity(); uint16_t ir,full; ir = lum >> 16 ; full = lum & 0xFFFF ; return tsl.calculateLux(full, ir); }
程式碼中define了幾個常量1 2 3 4 5 6 #define SEALEVELPRESSURE_HPA (1001.25) #define GAIN TSL2591_GAIN_HIGH #define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_600MS #define DELAY_TIME 10000
首先第一行定義了海平面的氣壓,為了計算高度用(高度計算的原理是測量氣壓減去海平面氣壓,高度升高10m約降低1hPa),可以從氣象局官網查詢,也能設為一大氣壓。第二行是TSL2591的Gain,由於我們要測量黑暗天空的品質(SQM),因此將Gain調成HIGH來測量極低環境下的光源,另外也將INTEGRATIONTIME調到最長。第四行則代表每30秒測量一次數據,這裡要注意溫溼度測量的採樣頻率不要太高,避免感測器升溫影響數據 。
官方建議量測間隔不要低於1秒,否則感測器可能會受到電流加熱影響
將數據顯示在TFT螢幕上 大部分的功能可以參照上方寫的,這裡要注意的是這次選的螢幕解析度只有160x128,因此要衡量一下版面怎麼配置,才能最好的顯示出來。比較困難的是顯示圖片,這次我打算在螢幕上顯示幾個不同的icon,因此需要用到Adafruit GFX函式庫裡的drawBitmap()。詳細教學可以參考這部影片 ,我在與程式碼同個資料夾下建立新檔案icon.c
,內容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <pgmspace.h> const unsigned char icon_temp[] PROGMEM = { 0x00 ,0x07 ,0xE0 ,0x00 , 0x00 ,0x0E ,0x70 ,0x00 , 0x00 ,0x18 ,0x18 ,0x00 , 0x00 ,0x11 ,0x88 ,0x00 , 0x01 ,0x93 ,0xC9 ,0x80 , 0x01 ,0xF2 ,0x4F ,0x80 , 0x00 ,0x32 ,0x4C ,0x00 , 0x00 ,0x32 ,0x4C ,0x00 , 0x07 ,0xB2 ,0x4D ,0xE0 , 0x07 ,0xF2 ,0x4F ,0xE0 , 0x00 ,0x32 ,0x4C ,0x00 , 0x00 ,0x32 ,0x4C ,0x00 , 0x01 ,0xB3 ,0xC9 ,0x80 , 0x01 ,0xB3 ,0xC1 ,0x80 , 0x00 ,0x32 ,0x40 ,0x00 , 0x00 ,0x32 ,0x48 ,0x00 , 0x07 ,0xB2 ,0x41 ,0xE0 , 0x07 ,0xB2 ,0x49 ,0xE0 , 0x00 ,0x32 ,0x4C ,0x00 , 0x00 ,0x32 ,0x4C ,0x00 , 0x00 ,0x62 ,0x46 ,0x00 , 0x00 ,0x46 ,0x62 ,0x00 , 0x00 ,0xCC ,0x33 ,0x00 , 0x00 ,0xC8 ,0x13 ,0x00 , 0x00 ,0xC8 ,0x13 ,0x00 , 0x00 ,0xC8 ,0x13 ,0x00 , 0x00 ,0xCC ,0x33 ,0x00 , 0x00 ,0x67 ,0xE6 ,0x00 , 0x00 ,0x63 ,0xC6 ,0x00 , 0x00 ,0x30 ,0x0C ,0x00 , 0x00 ,0x1C ,0x38 ,0x00 , 0x00 ,0x07 ,0xE0 ,0x00 };
裡面是用陣列存放每個像素的值,關於將圖片轉成陣列,可以使用這個網站 。注意第一行的#include <pgmspace.h>
要寫,因為ESP32的RAM很小,加上這行讓這些圖片陣列能使用空間較大的program memory。
經過完一番調整,終於完成了基本的版面,上面顯示各種數值,分別是溫度、SQM數值、濕度、氣壓、露點溫度以及海拔。我設定成10秒更新一次,在螢幕刷新上我注意到螢幕會有閃爍問題,因為每次刷新都需要將整個螢幕塗黑再一一畫上去,加上響應速度不快,導致閃爍(Flickering)。如果不使用自訂字型,去用預設的字型的話,可以只更新數字的部分,而不是將整張畫面重畫,能大幅降低閃爍情形,可惜預設字型很醜且大小不適合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 #include <math.h> #include <Wire.h> #include <SPI.h> #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> #include <Adafruit_TSL2591.h> #include <Adafruit_GFX.h> #include <Adafruit_ST7735.h> #include <Fonts/FreeSans9pt7b.h> #define SEALEVELPRESSURE_HPA 1003.0 #define GAIN TSL2591_GAIN_HIGH #define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_500MS #define EFFECTIVE_SOLID_ANGLE 1.532 #define DELAY_TIME 10000 #define TFT_CS 5 #define TFT_DC 17 #define TFT_RST 0 #define TFT_Vcc 16 #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define WHITE 0xFFFF #define GREY 0xD6BA Adafruit_BME280 bme; Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591 ); Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); float temp, humidity, pressure, altitude, luminosity, dewpoint, SQM;extern uint8_t icon_temp[];extern uint8_t icon_humidity[];extern uint8_t icon_pressure[];extern uint8_t icon_SQM[];extern uint8_t icon_dew[];extern uint8_t icon_altitude[];extern uint8_t icon_wifi[];void setup () { Serial.begin(9600 ); pinMode(TFT_Vcc, OUTPUT); digitalWrite(TFT_Vcc, 1 ); init_bme280(); init_tsl2591(); init_tft(); } void loop () { temp = get_temperature(); humidity = get_humidity(); pressure = get_pressure(); altitude = get_altitude(); luminosity = get_luminosity(); dewpoint = temp - ((100 -humidity)/5. ); SQM = log10f((luminosity/EFFECTIVE_SOLID_ANGLE)/108000 )/(-0.4 ); update_tft(temp, humidity, pressure, altitude, luminosity, dewpoint); Serial.print("Temp: " ); Serial.print(temp); Serial.println("*C" ); Serial.print("SQM: " ); Serial.print(SQM); Serial.println("Mag/as^2" ); Serial.print("Humid: " ); Serial.print(humidity); Serial.println("%" ); Serial.print("Pressure: " ); Serial.print(pressure); Serial.println("hPa" ); Serial.print("Alt: " ); Serial.print(altitude); Serial.println("m" ); Serial.print("DewPoint: " ); Serial.print(dewpoint); Serial.println("*C" ); Serial.print("Lumin: " ); Serial.print(luminosity); Serial.println("lux" ); Serial.println("" ); delay(DELAY_TIME); } void init_bme280 () { bme.begin(0x76 ); } void init_tsl2591 () { tsl.begin(0x29 ); tsl.setGain(GAIN); tsl.setTiming(INTEGRATIONTIME); } void init_tft () { tft.initR(INITR_BLACKTAB); tft.setRotation(1 ); tft.setTextWrap(false ); tft.fillScreen(ST77XX_BLACK); } float get_temperature () { return bme.readTemperature(); } float get_humidity () { return bme.readHumidity(); } float get_altitude () { return bme.readAltitude(SEALEVELPRESSURE_HPA); } float get_pressure () { return bme.readPressure()/100.0 ; } float get_luminosity () { uint32_t lum = tsl.getFullLuminosity(); uint16_t ir,full; ir = lum >> 16 ; full = lum & 0xFFFF ; return tsl.calculateLux(full, ir); } void update_tft (float temp, float humidity, float pressure, float altitude, float luminosity, float dewpoint) { tft.fillScreen(ST77XX_BLACK); tft.setFont(&FreeSans9pt7b); tft.setCursor(4 , 18 ); tft.setTextColor(ST77XX_WHITE); tft.print("Weather Station" ); tft.drawBitmap(140 , 4 , icon_wifi, 16 , 16 , GREEN); tft.drawLine(0 , 24 , 160 , 24 , WHITE); tft.drawBitmap(0 , 32 , icon_temp, 24 , 24 , WHITE); tft.setCursor(26 , 47 ); tft.setTextColor(ST77XX_WHITE); tft.print(temp); tft.setFont(NULL ); tft.setCursor(72 ,50 ); tft.print("C" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(80 , 32 , icon_SQM, 24 , 24 , WHITE); tft.setCursor(106 , 46 ); tft.setTextColor(ST77XX_WHITE); tft.print(SQM); tft.setFont(NULL ); tft.setCursor(108 ,49 ); tft.print("Mag/as2" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(0 , 66 , icon_humidity, 24 , 24 , WHITE); tft.setCursor(26 , 84 ); tft.setTextColor(ST77XX_WHITE); tft.print(humidity); tft.setFont(NULL ); tft.setCursor(72 ,88 ); tft.print("%" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(80 , 66 , icon_pressure, 24 , 24 , WHITE); tft.setCursor(105 , 84 ); tft.setTextColor(ST77XX_WHITE); tft.print(pressure, 1 ); tft.setFont(NULL ); tft.setCursor(140 ,88 ); tft.print("hPa" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(0 , 100 , icon_dew, 24 , 24 , WHITE); tft.setCursor(26 , 117 ); tft.setTextColor(ST77XX_WHITE); tft.print(dewpoint); tft.setFont(NULL ); tft.setCursor(72 ,120 ); tft.print("C" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(80 , 100 , icon_altitude, 24 , 24 , WHITE); tft.setCursor(105 , 117 ); tft.setTextColor(ST77XX_WHITE); tft.print(altitude); tft.setFont(NULL ); tft.setCursor(152 ,119 ); tft.print("m" ); tft.setFont(&FreeSans9pt7b); }
裡面有透過溫溼度去換算露點溫度,以及SQM數值的計算。
露點溫度及SQM數值計算 露點溫度 當環境溫度低於露點溫度時,空氣中的水氣就會凝結,這對進行天文攝影的人來說影響很大,有可能會讓相機和望遠鏡進水,因此新增了這一項有用的資訊,露點溫度能從溫度和濕度去計算,較簡單的近似是
$T_{d}$是露點溫度,$T$是環境溫度,$RH$是相對溼度,這條近似的式子在濕度50%以上的誤差可以小於+-1度,算是很好的準確度。
SQM數值 SQM數值是科學家提出來用來測量夜空天空背景亮度的單位,單位是$Magnitude/arcsec^{2}$,單位面積下的亮度。一般來說業餘天文家會用加拿大公司 Unihedron 生產的 SQM (Sky Quality Meter) 來測量SQM數值,這種儀器價格約落在119~135美元,出廠都有經過儀器校正,且體積小方便攜帶,我自己也有一台,去戶外觀星時都會隨時測量天空亮度,來衡量光害汙染程度。
偵測器上藍藍一片的是UV-IR截止濾鏡,用來濾掉紅外和紫外光,只允許可見光通過,以符合肉眼目視的範圍。裡面用的光感測器是TSL237S-LF,偵測暗光能力不錯,但因年代久遠,目前很難找到貨源。
網路上搜尋光害地圖,就能看到世界各地光害汙染程度,這些測量就是以SQM數值去統計和繪製,台灣的鹿林天文台,合歡山暗光公園,都有SQM監測裝置,可以研究當地的光汙染變化。
SQM數值的計算可以從光照度(lux)來換算,首先要先將光照度(Illuminance)換成光度(luminance)
其中$sr$是立體角(solid angle),這個角度與你感測器所能偵測的張角有關,一般SQM機器會偵測天空直徑範圍80度的區域,這區域裡的光會進到感測器裡,而附有鏡頭的SQM-L,則是偵測10度範圍的天空。官網 有提到它們販售的SQM的立體角是1.532sr,根據儀器設計不同,這個立體角也會不同。
上面的公式就是SQM數值的定義,一般的夜空亮度數值落在16~22,我先前去鹿林天文台的時候,有量到超過22的數值,光害極小,能輕易看見銀河和數不盡的星星。
第一次測出來的結果與我手邊的SQM儀器相差不遠,但是仍需要校正,等到做好了外殼,到時修改預設的立體角就能校正了,要注意光感測元件不要傾斜,盡量與開口平行,否會導致測到的光大幅下降,失去準度。
網路(IoT)功能 這台NodeMCU-32S最特別的就是它有內建Wi-Fi和藍芽晶片,當然要讓我們測到的資料有能力傳送到網路上阿。接下來將介紹這台的連網功能,並結合Adafruit來達成網頁監測數據的功能。
首先先安裝MQTT函式庫
從feed頁面查看名稱,點開後看網址後綴,feed名稱的空格會被-替換掉,大寫變成小寫,需要注意 。這裡的名稱就是我們要寫入程式碼的feed名稱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 #include <math.h> #include <Wire.h> #include <SPI.h> #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> #include <Adafruit_TSL2591.h> #include <Adafruit_GFX.h> #include <Adafruit_ST7735.h> #include <Fonts/FreeSans9pt7b.h> #include <WiFi.h> #include <Adafruit_MQTT.h> #include <Adafruit_MQTT_Client.h> #define WLAN_SSID "Wooster" #define WLAN_PASSWD "123456789" #define WLAN_TIMEOUT_MS 15000 #define AIO_SERVER "io.adafruit.com" #define AIO_SERVERPORT 1883 #define AIO_USERNAME "jamiechang917" #define AIO_KEY "填入你的金鑰(key)" #define SEALEVELPRESSURE_HPA 1003.0 #define GAIN TSL2591_GAIN_HIGH #define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_500MS #define EFFECTIVE_SOLID_ANGLE 1.532 #define SCREEN_REFRESH_TIME 10 #define SEND_DATA_TIME 15 #define TFT_CS 5 #define TFT_DC 17 #define TFT_RST 0 #define TFT_Vcc 16 #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define WHITE 0xFFFF #define GREY 0xD6BA WiFiClient client; Adafruit_MQTT_Client mqtt (&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY) ;Adafruit_MQTT_Publish AIO_temp = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/temperature" ); Adafruit_MQTT_Publish AIO_humidity = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/humidity" ); Adafruit_MQTT_Publish AIO_SQM = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/sqm" ); Adafruit_MQTT_Publish AIO_pressure = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/pressure" ); Adafruit_MQTT_Publish AIO_altitude = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/altitude" ); Adafruit_MQTT_Publish AIO_dewpoint = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/dew-point" ); Adafruit_BME280 bme; Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591 ); Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); float temp, humidity, pressure, altitude, luminosity, dewpoint, SQM;int refresh_cnt = 0 , send_cnt = 0 ; extern uint8_t icon_temp[];extern uint8_t icon_humidity[];extern uint8_t icon_pressure[];extern uint8_t icon_SQM[];extern uint8_t icon_dew[];extern uint8_t icon_altitude[];extern uint8_t icon_wifi[];void setup () { Serial.begin(9600 ); pinMode(TFT_Vcc, OUTPUT); digitalWrite(TFT_Vcc, 1 ); init_bme280(); init_tsl2591(); init_tft(); measure(); update_tft(temp, humidity, pressure, altitude, dewpoint, SQM); connect_wifi(); connect_adafruit(); } void loop () { if (refresh_cnt >= SCREEN_REFRESH_TIME) { refresh_cnt = 0 ; measure(); update_tft(temp, humidity, pressure, altitude, dewpoint, SQM); } if (send_cnt >= SEND_DATA_TIME) { send_cnt = 0 ; measure(); if (!mqtt.connected()) { Serial.println("Reconnecting..." ); connect_wifi(); connect_adafruit(); } send_data(temp, humidity, pressure, altitude, dewpoint, SQM); } refresh_cnt += 1 ; send_cnt += 1 ; delay(1000 ); } void init_bme280 () { bme.begin(0x76 ); } void init_tsl2591 () { tsl.begin(0x29 ); tsl.setGain(GAIN); tsl.setTiming(INTEGRATIONTIME); } void init_tft () { tft.initR(INITR_BLACKTAB); tft.setRotation(1 ); tft.setTextWrap(false ); tft.fillScreen(ST77XX_BLACK); } float get_temperature () { return bme.readTemperature(); } float get_humidity () { return bme.readHumidity(); } float get_altitude () { return bme.readAltitude(SEALEVELPRESSURE_HPA); } float get_pressure () { return bme.readPressure()/100.0 ; } float get_luminosity () { uint32_t lum = tsl.getFullLuminosity(); uint16_t ir,full; ir = lum >> 16 ; full = lum & 0xFFFF ; return tsl.calculateLux(full, ir); } void measure () { temp = get_temperature(); humidity = get_humidity(); pressure = get_pressure(); altitude = get_altitude(); luminosity = get_luminosity(); dewpoint = temp - ((100 -humidity)/5. ); SQM = log10f((luminosity/EFFECTIVE_SOLID_ANGLE)/108000 )/(-0.4 ); Serial.println("=============Measurement=============" ); Serial.print("Temperature:\t" ); Serial.print(temp); Serial.println(" C" ); Serial.print("SQM:\t" ); Serial.print(SQM); Serial.println(" Mag/as^2" ); Serial.print("Humidity:\t" ); Serial.print(humidity); Serial.println(" %" ); Serial.print("Pressure:\t" ); Serial.print(pressure); Serial.println(" hPa" ); Serial.print("Altitude:\t" ); Serial.print(altitude); Serial.println(" m" ); Serial.print("Dew Point:\t" ); Serial.print(dewpoint); Serial.println(" C" ); Serial.print("Illuminance:\t" ); Serial.print(luminosity); Serial.println(" lux" ); Serial.println("=====================================" ); Serial.println("" ); } void update_tft (float temp, float humidity, float pressure, float altitude, float dewpoint, float SQM) { tft.fillScreen(ST77XX_BLACK); tft.setFont(&FreeSans9pt7b); tft.setCursor(4 , 18 ); tft.setTextColor(ST77XX_WHITE); tft.print("Weather Station" ); if (WiFi.status() != WL_CONNECTED) { tft.drawBitmap(140 , 4 , icon_wifi, 16 , 16 , RED); } else { tft.drawBitmap(140 , 4 , icon_wifi, 16 , 16 , GREEN); } tft.drawLine(0 , 24 , 160 , 24 , WHITE); tft.drawBitmap(0 , 32 , icon_temp, 24 , 24 , WHITE); tft.setCursor(26 , 47 ); tft.setTextColor(ST77XX_WHITE); tft.print(temp); tft.setFont(NULL ); tft.setCursor(72 ,50 ); tft.print("C" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(80 , 32 , icon_SQM, 24 , 24 , WHITE); tft.setCursor(106 , 46 ); tft.setTextColor(ST77XX_WHITE); tft.print(SQM); tft.setFont(NULL ); tft.setCursor(108 ,49 ); tft.print("Mag/as2" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(0 , 66 , icon_humidity, 24 , 24 , WHITE); tft.setCursor(26 , 84 ); tft.setTextColor(ST77XX_WHITE); tft.print(humidity); tft.setFont(NULL ); tft.setCursor(72 ,88 ); tft.print("%" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(80 , 66 , icon_pressure, 24 , 24 , WHITE); tft.setCursor(105 , 84 ); tft.setTextColor(ST77XX_WHITE); tft.print(pressure, 1 ); tft.setFont(NULL ); tft.setCursor(140 ,88 ); tft.print("hPa" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(0 , 100 , icon_dew, 24 , 24 , WHITE); tft.setCursor(26 , 117 ); tft.setTextColor(ST77XX_WHITE); tft.print(dewpoint); tft.setFont(NULL ); tft.setCursor(72 ,120 ); tft.print("C" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(80 , 100 , icon_altitude, 24 , 24 , WHITE); tft.setCursor(105 , 117 ); tft.setTextColor(ST77XX_WHITE); tft.print(altitude); tft.setFont(NULL ); tft.setCursor(152 ,119 ); tft.print("m" ); tft.setFont(&FreeSans9pt7b); } void connect_wifi () { Serial.println("===========WiFi Connection===========" ); WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(100 ); Serial.println("WiFi setup done" ); WiFi.begin(WLAN_SSID, WLAN_PASSWD); delay(WLAN_TIMEOUT_MS); if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connection failed!" ); } else { Serial.println("WiFi connection successed!" ); Serial.print("IP: " ); Serial.println(WiFi.localIP()); } Serial.println("=====================================" ); Serial.println("" ); } void send_data (float temp, float humidity, float pressure, float altitude, float dewpoint, float SQM) { Serial.println("======Send data to Adafruit IO======" ); if (AIO_temp.publish(temp)) { Serial.println("Sent data!" ); } else { Serial.println("Failed to send data!" ); } AIO_humidity.publish(humidity); AIO_pressure.publish(pressure); AIO_altitude.publish(altitude); AIO_SQM.publish(SQM); AIO_dewpoint.publish(dewpoint); Serial.println("=====================================" ); Serial.println("" ); } void connect_adafruit () { Serial.println("======Connecting to Adafruit IO======" ); int8_t ret; if ((ret = mqtt.connect()) != 0 ) { switch (ret) { case 1 : Serial.println(F("[ERROR] Wrong protocol" )); break ; case 2 : Serial.println(F("[ERROR] ID rejected" )); break ; case 3 : Serial.println(F("[ERROR] Server unavailable" )); break ; case 4 : Serial.println(F("[ERROR] Bad username/password" )); break ; case 5 : Serial.println(F("[ERROR] Not authorized" )); break ; case 6 : Serial.println(F("[ERROR] Failed to subscribe" )); break ; default : Serial.println(F("[ERROR] Connection failed" )); break ; } } Serial.println("=====================================" ); Serial.println("" ); }
我們從程式碼一一來看,這裡新增了WiFi聯網功能以及Adafruit IO的資料推送功能1 2 3 4 5 6 7 8 9 10 11 12 13 #include <WiFi.h> #include <Adafruit_MQTT.h> #include <Adafruit_MQTT_Client.h> #define WLAN_SSID "Wooster" #define WLAN_PASSWD "123456789" #define WLAN_TIMEOUT_MS 15000 #define AIO_SERVER "io.adafruit.com" #define AIO_SERVERPORT 1883 #define AIO_USERNAME "jamiechang917" #define AIO_KEY "填入你的金鑰(key)"
WLAN_SSID
是你的WiFi名稱,下面則是密碼,需要寫在程式碼裡面。另外Adafruit IO的Server網址及端口也要寫,下面則是Adafruit帳號和金鑰。
1 2 #define SCREEN_REFRESH_TIME 10 #define SEND_DATA_TIME 15
這兩行是我自己定義的,我將螢幕刷新時間設定為10秒,而發送資料到Adafruit IO的時間間隔為15秒。要實現這種每隔幾秒做一件事情的功能很簡單,我們先定義兩個計數器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int refresh_cnt = 0 , send_cnt = 0 ;void loop () { if (refresh_cnt >= SCREEN_REFRESH_TIME) { refresh_cnt = 0 ; } if (send_cnt >= SEND_DATA_TIME) { send_cnt = 0 ; } refresh_cnt += 1 ; send_cnt += 1 ; delay(1000 ); }
在loop()
迴圈裡,我設定delay(1000)
,這樣一來每隔一秒這兩個計數器就會加一,而當他們達到上限時,就會進入相對應的程式碼區塊。舉發送資料的例子,當計數器到15的時候就開始發送資料,而動作結束後這個計數器也會重新歸零。如此反覆循環就能達成每隔15秒發送一次資料的功能。
1 2 3 4 5 6 7 8 WiFiClient client; Adafruit_MQTT_Client mqtt (&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY) ;Adafruit_MQTT_Publish AIO_temp = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/temperature" ); Adafruit_MQTT_Publish AIO_humidity = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/humidity" ); Adafruit_MQTT_Publish AIO_SQM = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/sqm" ); Adafruit_MQTT_Publish AIO_pressure = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/pressure" ); Adafruit_MQTT_Publish AIO_altitude = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/altitude" ); Adafruit_MQTT_Publish AIO_dewpoint = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/dew-point" );
開頭兩行分別建立WiFi以及MQTT的Client。另外其他的是feed,在Adafruit的dashboard裡,要監測的變量稱為feed,在程式碼裡要建立Adafruit_MQTT_Publish
類的變數,格式如上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void connect_wifi () { Serial.println("===========WiFi Connection===========" ); WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(100 ); Serial.println("WiFi setup done" ); WiFi.begin(WLAN_SSID, WLAN_PASSWD); delay(WLAN_TIMEOUT_MS); if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connection failed!" ); } else { Serial.println("WiFi connection successed!" ); Serial.print("IP: " ); Serial.println(WiFi.localIP()); } Serial.println("=====================================" ); Serial.println("" ); }
我將連結WiFi的基本步驟寫成一個函式,首先WiFi.mode()
是設定WiFi晶片的模式,可以是STA或是AP模式,如果是要發送資料到網路上,需要使用STA模式。連接WiFi的話使用WiFi.begin(WLAN_SSID, WLAN_PASSWD)
,分別填入SSID和密碼,要注意的是下面一行的delay(WLAN_TIMEOUT_MS)
,這裡我設定15秒,千萬不要設定太短,否則NodeMCU-32S將會來不及連接WiFi,導致永遠連不上的狀況 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void connect_adafruit () { Serial.println("======Connecting to Adafruit IO======" ); int8_t ret; if ((ret = mqtt.connect()) != 0 ) { switch (ret) { case 1 : Serial.println(F("[ERROR] Wrong protocol" )); break ; case 2 : Serial.println(F("[ERROR] ID rejected" )); break ; case 3 : Serial.println(F("[ERROR] Server unavailable" )); break ; case 4 : Serial.println(F("[ERROR] Bad username/password" )); break ; case 5 : Serial.println(F("[ERROR] Not authorized" )); break ; case 6 : Serial.println(F("[ERROR] Failed to subscribe" )); break ; default : Serial.println(F("[ERROR] Connection failed" )); break ; } } else { Serial.println("Adafruit IO connection successed!" ) } Serial.println("=====================================" ); Serial.println("" ); }
這部分是連接Adafruit IO的步驟,再連接完WiFi後使用。方式很簡單,調用mqtt.connect()
就可以了,這個函式會返回一個整數,0代表成功,其他數字代表不同的連接狀況,如果發生狀況,我將狀況印在Serial Monitor上,方便debug。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void send_data (float temp, float humidity, float pressure, float altitude, float dewpoint, float SQM) { Serial.println("======Send data to Adafruit IO======" ); if (AIO_temp.publish(temp)) { Serial.println("Sent data!" ); } else { Serial.println("Failed to send data!" ); } AIO_humidity.publish(humidity); AIO_pressure.publish(pressure); AIO_altitude.publish(altitude); AIO_SQM.publish(SQM); AIO_dewpoint.publish(dewpoint); Serial.println("=====================================" ); Serial.println("" ); }
當連結到Adafruit IO後,將先前建立的Adafruit_MQTT_Publish
類型變數後方加上.publish
就可以發布資料到該feed。
成品與心得 最近因疫情嚴峻,想買個零件都要等超過一星期,而且一開始挑選的一些零件甚至沒貨,只好另尋替代品,不過經過一番調整,總算做出一個土炮攜帶型天氣站了,我暫時用樂高拼了一個殼給它,未來再用3D列印給它更好的家。
初步校正後,我關燈比較看看SQM和天氣站的數值,看起來是相當接近阿,我個人很滿意,而且未來還能去山上進行更精細的調教。
最困難的地方我覺得還是螢幕版面的設計,要自己找icon還要在有限的空間內塞入這麼多東西,花了不少時間才弄好,但是做完後實在是賞心悅目,一眼就能看到所有資訊,打開WiFi還能上傳資料,以後在山上天冷了也能躲在車裡看,不用坐在外頭吹冷風 :)。
最後附上程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 #include <math.h> #include <Wire.h> #include <SPI.h> #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> #include <Adafruit_TSL2591.h> #include <Adafruit_GFX.h> #include <Adafruit_ST7735.h> #include <Fonts/FreeSans9pt7b.h> #include <WiFi.h> #include <Adafruit_MQTT.h> #include <Adafruit_MQTT_Client.h> #define WLAN_SSID "Wooster" #define WLAN_PASSWD "123456789" #define WLAN_TIMEOUT_MS 15000 #define AIO_SERVER "io.adafruit.com" #define AIO_SERVERPORT 1883 #define AIO_USERNAME "jamiechang917" #define AIO_KEY "AIO金鑰" #define SEALEVELPRESSURE_HPA 1003.0 #define GAIN TSL2591_GAIN_HIGH #define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_500MS #define EFFECTIVE_SOLID_ANGLE 1.4 #define CALIBRATION_COEFF_A 1.0 #define CALIBRATION_COEFF_B 0.0 #define SCREEN_REFRESH_TIME 10 #define SEND_DATA_TIME 15 #define RECONNECTION_TIME 60 #define LOOP_DELAY_TIME 0.1 #define TFT_CS 5 #define TFT_DC 17 #define TFT_RST 0 #define TFT_Vcc 16 #define Button 4 #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define WHITE 0xFFFF #define GREY 0xD6BA WiFiClient client; Adafruit_MQTT_Client mqtt (&client, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY) ;Adafruit_MQTT_Publish AIO_temp = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/temperature" ); Adafruit_MQTT_Publish AIO_humidity = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/humidity" ); Adafruit_MQTT_Publish AIO_SQM = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/sqm" ); Adafruit_MQTT_Publish AIO_pressure = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/pressure" ); Adafruit_MQTT_Publish AIO_altitude = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/altitude" ); Adafruit_MQTT_Publish AIO_dewpoint = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/dew-point" ); Adafruit_BME280 bme; Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591 ); Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); float temp, humidity, pressure, altitude, luminosity, dewpoint, SQM;int refresh_cnt = 0 , send_cnt = 0 , reconnect_cnt = 0 ; int display_state = 1 , button_state = 1 , old_button_state = 1 ;extern uint8_t icon_temp[];extern uint8_t icon_humidity[];extern uint8_t icon_pressure[];extern uint8_t icon_SQM[];extern uint8_t icon_dew[];extern uint8_t icon_altitude[];extern uint8_t icon_wifi[];void setup () { Serial.begin(115200 ); pinMode(TFT_Vcc, OUTPUT); digitalWrite(TFT_Vcc, HIGH); pinMode(Button, INPUT); digitalWrite(LED_BUILTIN, LOW); init_bme280(); init_tsl2591(); init_tft(); measure(); update_tft(); connect_wifi(); connect_adafruit(); } void loop () { old_button_state = button_state; button_state = digitalRead(Button); if (button_state == 0 && old_button_state == 1 ) { display_state = 1 - display_state; digitalWrite(TFT_Vcc, display_state); if (display_state == 1 ) { init_tft(); update_tft(); } } if (refresh_cnt >= SCREEN_REFRESH_TIME/LOOP_DELAY_TIME) { refresh_cnt = 0 ; measure(); update_tft(); } if (send_cnt >= SEND_DATA_TIME/LOOP_DELAY_TIME) { send_cnt = 0 ; measure(); send_data(); } if (reconnect_cnt >= RECONNECTION_TIME/LOOP_DELAY_TIME) { reconnect_cnt = 0 ; if (!mqtt.connected()) { Serial.println("Reconnecting..." ); connect_wifi(); connect_adafruit(); } } refresh_cnt += 1 ; send_cnt += 1 ; reconnect_cnt += 1 ; delay(LOOP_DELAY_TIME*1000 ); } void init_bme280 () { bme.begin(0x76 ); } void init_tsl2591 () { tsl.begin(0x29 ); tsl.setGain(GAIN); tsl.setTiming(INTEGRATIONTIME); } void init_tft () { tft.initR(INITR_BLACKTAB); tft.setRotation(1 ); tft.setTextWrap(false ); tft.fillScreen(ST77XX_BLACK); } float get_temperature () { return bme.readTemperature(); } float get_humidity () { return bme.readHumidity(); } float get_altitude () { return bme.readAltitude(SEALEVELPRESSURE_HPA); } float get_pressure () { return bme.readPressure()/100.0 ; } float get_luminosity () { uint32_t lum = tsl.getFullLuminosity(); uint16_t ir,full; ir = lum >> 16 ; full = lum & 0xFFFF ; return tsl.calculateLux(full, ir); } void measure () { temp = get_temperature(); humidity = get_humidity(); pressure = get_pressure(); altitude = get_altitude(); luminosity = get_luminosity(); dewpoint = temp - ((100 -humidity)/5. ); SQM = 12.5836 + CALIBRATION_COEFF_A*log10f(luminosity/EFFECTIVE_SOLID_ANGLE)/(-0.4 ) + CALIBRATION_COEFF_B; Serial.println("=============Measurement=============" ); Serial.print("Temperature:\t" ); Serial.print(temp); Serial.println(" C" ); Serial.print("SQM Value:\t" ); Serial.print(SQM); Serial.println(" Mag/as^2" ); Serial.print("Humidity:\t" ); Serial.print(humidity); Serial.println(" %" ); Serial.print("Pressure:\t" ); Serial.print(pressure); Serial.println(" hPa" ); Serial.print("Altitude:\t" ); Serial.print(altitude); Serial.println(" m" ); Serial.print("Dew Point:\t" ); Serial.print(dewpoint); Serial.println(" C" ); Serial.print("Illuminance:\t" ); Serial.print(luminosity,6 ); Serial.println(" lux" ); Serial.println("=====================================" ); Serial.println("" ); } void update_tft () { tft.fillScreen(ST77XX_BLACK); tft.setFont(&FreeSans9pt7b); tft.setCursor(4 , 18 ); tft.setTextColor(ST77XX_WHITE); tft.print("Weather Station" ); if (WiFi.status() != WL_CONNECTED) { tft.drawBitmap(140 , 4 , icon_wifi, 16 , 16 , RED); } else { tft.drawBitmap(140 , 4 , icon_wifi, 16 , 16 , GREEN); } tft.drawLine(0 , 24 , 160 , 24 , WHITE); tft.drawBitmap(0 , 32 , icon_temp, 24 , 24 , WHITE); tft.setCursor(26 , 47 ); tft.setTextColor(ST77XX_WHITE); tft.print(temp); tft.setFont(NULL ); tft.setCursor(72 ,50 ); tft.print("C" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(80 , 32 , icon_SQM, 24 , 24 , WHITE); tft.setCursor(106 , 46 ); tft.setTextColor(ST77XX_WHITE); tft.print(SQM); tft.setFont(NULL ); tft.setCursor(108 ,49 ); tft.print("Mag/as2" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(0 , 66 , icon_humidity, 24 , 24 , WHITE); tft.setCursor(26 , 84 ); tft.setTextColor(ST77XX_WHITE); tft.print(humidity); tft.setFont(NULL ); tft.setCursor(72 ,88 ); tft.print("%" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(80 , 66 , icon_pressure, 24 , 24 , WHITE); tft.setCursor(105 , 84 ); tft.setTextColor(ST77XX_WHITE); tft.print(pressure, 1 ); tft.setFont(NULL ); tft.setCursor(140 ,88 ); tft.print("hPa" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(0 , 100 , icon_dew, 24 , 24 , WHITE); tft.setCursor(26 , 117 ); tft.setTextColor(ST77XX_WHITE); tft.print(dewpoint); tft.setFont(NULL ); tft.setCursor(72 ,120 ); tft.print("C" ); tft.setFont(&FreeSans9pt7b); tft.drawBitmap(80 , 100 , icon_altitude, 24 , 24 , WHITE); tft.setCursor(105 , 117 ); tft.setTextColor(ST77XX_WHITE); tft.print(altitude); tft.setFont(NULL ); tft.setCursor(152 ,119 ); tft.print("m" ); tft.setFont(&FreeSans9pt7b); } void connect_wifi () { Serial.println("===========WiFi Connection===========" ); WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(100 ); Serial.println("WiFi setup done" ); WiFi.begin(WLAN_SSID, WLAN_PASSWD); delay(WLAN_TIMEOUT_MS); if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connection failed!" ); } else { Serial.println("WiFi connection successed!" ); Serial.print("IP: " ); Serial.println(WiFi.localIP()); } Serial.println("=====================================" ); Serial.println("" ); } void send_data () { Serial.println("======Send data to Adafruit IO======" ); if (AIO_temp.publish(temp)) { Serial.println("Sent data!" ); } else { Serial.println("Failed to send data!" ); } AIO_humidity.publish(humidity); AIO_pressure.publish(pressure); AIO_altitude.publish(altitude); AIO_SQM.publish(SQM); AIO_dewpoint.publish(dewpoint); Serial.println("=====================================" ); Serial.println("" ); } void connect_adafruit () { Serial.println("======Connecting to Adafruit IO======" ); int8_t ret; if ((ret = mqtt.connect()) != 0 ) { switch (ret) { case 1 : Serial.println(F("[ERROR] Wrong protocol" )); break ; case 2 : Serial.println(F("[ERROR] ID rejected" )); break ; case 3 : Serial.println(F("[ERROR] Server unavailable" )); break ; case 4 : Serial.println(F("[ERROR] Bad username/password" )); break ; case 5 : Serial.println(F("[ERROR] Not authorized" )); break ; case 6 : Serial.println(F("[ERROR] Failed to subscribe" )); break ; default : Serial.println(F("[ERROR] Connection failed" )); break ; } } else { Serial.println("Adafruit IO connection successed!" ); } Serial.println("=====================================" ); Serial.println("" ); }
更多功能