天文用攜帶型天氣站

製作日期: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>

/*#include <SPI.h>
#define BME_SCK 18
#define BME_MISO 19
#define BME_MOSI 23
#define BME_CS 5*/

#define SEALEVELPRESSURE_HPA (1013.25)

Adafruit_BME280 bme; // I2C
//Adafruit_BME280 bme(BME_CS); // hardware SPI
//Adafruit_BME280 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK); // software SPI

unsigned long delayTime;

void setup() {
Serial.begin(9600);
Serial.println(F("BME280 test"));

bool status;

// default settings
// (you can also pass in a Wire library object like &Wire2)
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");

// Convert temperature to Fahrenheit
/*Serial.print("Temperature = ");
Serial.print(1.8 * bme.readTemperature() + 32);
Serial.println(" *F");*/

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
/* TSL2591 Digital Light Sensor */
/* Dynamic Range: 600M:1 */
/* Maximum Lux: 88K */

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_TSL2591.h"

// Example for demonstrating the TSL2591 library - public domain!

// connect SCL to I2C Clock
// connect SDA to I2C Data
// connect Vin to 3.3-5V DC
// connect GROUND to common ground

Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591); // pass in a number for the sensor identifier (for your use later)


/**************************************************************************/
/*
Configures the gain and integration time for the TSL2591
*/
/**************************************************************************/
void configureSensor(void)
{
// You can change the gain on the fly, to adapt to brighter/dimmer light situations
//tsl.setGain(TSL2591_GAIN_LOW); // 1x gain (bright light)
tsl.setGain(TSL2591_GAIN_MED); // 25x gain
//tsl.setGain(TSL2591_GAIN_HIGH); // 428x gain

// Changing the integration time gives you a longer time over which to sense light
// longer timelines are slower, but are good in very low light situtations!
//tsl.setTiming(TSL2591_INTEGRATIONTIME_100MS); // shortest integration time (bright light)
// tsl.setTiming(TSL2591_INTEGRATIONTIME_200MS);
tsl.setTiming(TSL2591_INTEGRATIONTIME_300MS);
// tsl.setTiming(TSL2591_INTEGRATIONTIME_400MS);
// tsl.setTiming(TSL2591_INTEGRATIONTIME_500MS);
// tsl.setTiming(TSL2591_INTEGRATIONTIME_600MS); // longest integration time (dim light)

/* Display the gain and integration time for reference sake */
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);
}


/* Configure the sensor */
configureSensor();
}

/**************************************************************************/
/*
Shows how to perform a basic read on visible, full spectrum or
infrared light (returns raw 16-bit ADC values)
*/
/**************************************************************************/
void simpleRead(void)
{
// Simple data read example. Just read the infrared, fullspecrtrum diode
// or 'visible' (difference between the two) channels.
// This can take 100-600 milliseconds! Uncomment whichever of the following you want to read
uint16_t x = tsl.getLuminosity(TSL2591_VISIBLE);
//uint16_t x = tsl.getLuminosity(TSL2591_FULLSPECTRUM);
//uint16_t x = tsl.getLuminosity(TSL2591_INFRARED);

Serial.print(F("[ ")); Serial.print(millis()); Serial.print(F(" ms ] "));
Serial.print(F("Luminosity: "));
Serial.println(x, DEC);
}

/**************************************************************************/
/*
Show how to read IR and Full Spectrum at once and convert to lux
*/
/**************************************************************************/
void advancedRead(void)
{
// More advanced data read example. Read 32 bits with top 16 bits IR, bottom 16 bits full spectrum
// That way you can do whatever math and comparisons you want!
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);
}

/**************************************************************************/
/*
Performs a read using the Adafruit Unified Sensor API.
*/
/**************************************************************************/
void unifiedSensorAPIRead(void)
{
/* Get a new sensor event */
sensors_event_t event;
tsl.getEvent(&event);

/* Display the results (light is measured in lux) */
Serial.print(F("[ ")); Serial.print(event.timestamp); Serial.print(F(" ms ] "));
if ((event.light == 0) |
(event.light > 4294966000.0) |
(event.light <-4294966000.0))
{
/* If event.light = 0 lux the sensor is probably saturated */
/* and no reliable data could be generated! */
/* if event.light is +/- 4294967040 there was a float over/underflow */
Serial.println(F("Invalid data (adjust gain or timing)"));
}
else
{
Serial.print(event.light); Serial.println(F(" lux"));
}
}


/**************************************************************************/
/*
Arduino loop function, called once 'setup' is complete (your own code
should go here)
*/
/**************************************************************************/
void loop(void)
{
//simpleRead();
advancedRead();
// unifiedSensorAPIRead();

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); //screen rotation
tft.fillScreen(ST77XX_BLACK); //filled with black color
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); //x,y,w,h,radius,color
tft.fillCircle(80, 110, 15, ST77XX_BLUE); //x,y,radius
tft.drawRect(105, 95, 30, 30,ST77XX_GREEN); //x,y,width,height
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); //screen rotation
tft.fillScreen(ST77XX_BLACK); //filled with black color
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() {
// put your setup code here, to run once:
Serial.begin(115200);
pinMode(LED, OUTPUT);
}

void loop() {
// put your main code here, to run repeatedly:
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) // 1 atm = 1013.25 hPa

#define GAIN TSL2591_GAIN_MED // (LOW, MID, HIGH)
#define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_300MS // (100MS,200MS,300MS,400MS,500MS,600MS)

#define DELAY_TIME 10000 // interval time for every measurement (ms)

Adafruit_BME280 bme; //I2C
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); // return -1 if the value blows up
}



程式碼中define了幾個常量

1
2
3
4
5
6
#define SEALEVELPRESSURE_HPA (1001.25) // 1 atm = 1013.25 hPa

#define GAIN TSL2591_GAIN_HIGH // (LOW, MID, HIGH)
#define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_600MS // (100MS,200MS,300MS,400MS,500MS,600MS)

#define DELAY_TIME 10000 // interval time for every measurement (ms)

首先第一行定義了海平面的氣壓,為了計算高度用(高度計算的原理是測量氣壓減去海平面氣壓,高度升高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
// icon.c
#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 // 1 atm = 1013.25 hPa

#define GAIN TSL2591_GAIN_HIGH // (LOW, MID, HIGH)
#define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_500MS // (100MS,200MS,300MS,400MS,500MS,600MS)
#define EFFECTIVE_SOLID_ANGLE 1.532 // solid angle in steradians for SQM

#define DELAY_TIME 10000 // interval time for every measurement

#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; //I2C
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;

// icon's bitmap
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.); // calculate dew point

//calculate SQM
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); // for 1.8" ST7735 TFT display
tft.setRotation(1); //screen rotation
tft.setTextWrap(false);
tft.fillScreen(ST77XX_BLACK); //filled with black color
}

float get_temperature() {
return bme.readTemperature();
}

float get_humidity() {
return bme.readHumidity();
}

float get_altitude() {
return bme.readAltitude(SEALEVELPRESSURE_HPA);
}

float get_pressure() { // return hPa
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); // return -1 if the value blows up
}

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>

//Wi-Fi parameters
#define WLAN_SSID "Wooster"
#define WLAN_PASSWD "123456789"
#define WLAN_TIMEOUT_MS 15000
// Adafruit IO
#define AIO_SERVER "io.adafruit.com"
#define AIO_SERVERPORT 1883
#define AIO_USERNAME "jamiechang917"
#define AIO_KEY "填入你的金鑰(key)"

#define SEALEVELPRESSURE_HPA 1003.0 // 1 atm = 1013.25 hPa

#define GAIN TSL2591_GAIN_HIGH // (LOW, MID, HIGH)
#define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_500MS // (100MS,200MS,300MS,400MS,500MS,600MS)
#define EFFECTIVE_SOLID_ANGLE 1.532 // solid angle in steradians for SQM

#define SCREEN_REFRESH_TIME 10 // (second) refresh the screen every 10 seconds
#define SEND_DATA_TIME 15 // (second) send data to adafruit io every 15 seconds

#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

//Setup the WiFi and MQTT client
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");

// setup the sensors
Adafruit_BME280 bme; //I2C
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;

// icon's bitmap
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();
// send data to adafruit io
if (!mqtt.connected()) { // reconnect when disconnecting
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); // for 1.8" ST7735 TFT display
tft.setRotation(1); //screen rotation
tft.setTextWrap(false);
tft.fillScreen(ST77XX_BLACK); //filled with black color
}

float get_temperature() {
return bme.readTemperature();
}

float get_humidity() {
return bme.readHumidity();
}

float get_altitude() {
return bme.readAltitude(SEALEVELPRESSURE_HPA);
}

float get_pressure() { // return hPa
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); // return -1 if the value blows up
}

void measure() {
temp = get_temperature();
humidity = get_humidity();
pressure = get_pressure();
altitude = get_altitude();
luminosity = get_luminosity();

// calculate dew point
dewpoint = temp - ((100-humidity)/5.);
// calculate SQM
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); // set to station mode
WiFi.disconnect(); // init
delay(100);
Serial.println("WiFi setup done");

WiFi.begin(WLAN_SSID, WLAN_PASSWD);
delay(WLAN_TIMEOUT_MS); // important, don't make the delay too short
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("");
}

//publish the data to Adafruit IO
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("");
}

// connect to Adafruit IO
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>

//Wi-Fi parameters
#define WLAN_SSID "Wooster"
#define WLAN_PASSWD "123456789"
#define WLAN_TIMEOUT_MS 15000
// Adafruit IO
#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 // (second) refresh the screen every 10 seconds
#define SEND_DATA_TIME 15 // (second) send data to adafruit io every 15 seconds

這兩行是我自己定義的,我將螢幕刷新時間設定為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); // set to station mode
WiFi.disconnect(); // init
delay(100);
Serial.println("WiFi setup done");

WiFi.begin(WLAN_SSID, WLAN_PASSWD);
delay(WLAN_TIMEOUT_MS); // important, don't make the delay too short
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
// connect to Adafruit IO
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
//publish the data to Adafruit IO
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>

//Wi-Fi parameters
#define WLAN_SSID "Wooster"
#define WLAN_PASSWD "123456789"
#define WLAN_TIMEOUT_MS 15000
// Adafruit IO
#define AIO_SERVER "io.adafruit.com"
#define AIO_SERVERPORT 1883
#define AIO_USERNAME "jamiechang917"
#define AIO_KEY "AIO金鑰"

#define SEALEVELPRESSURE_HPA 1003.0 // 1 atm = 1013.25 hPa

#define GAIN TSL2591_GAIN_HIGH // (LOW, MID, HIGH)
#define INTEGRATIONTIME TSL2591_INTEGRATIONTIME_500MS // (100MS,200MS,300MS,400MS,500MS,600MS)
#define EFFECTIVE_SOLID_ANGLE 1.4 // solid angle in steradians for SQM
#define CALIBRATION_COEFF_A 1.0 // SQM = 12.5836 - COEFF_A*log(lux/solid_angle)/(-0.4) + COEFF_B
#define CALIBRATION_COEFF_B 0.0

#define SCREEN_REFRESH_TIME 10 // (second) refresh the screen every 10 seconds
#define SEND_DATA_TIME 15 // (second) send data to adafruit io every 15 seconds
#define RECONNECTION_TIME 60 // (second) reconnect wifi every 60 seconds if disconnected

#define LOOP_DELAY_TIME 0.1 // (second) delay time in the loop() function

#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

//Setup the WiFi and MQTT client
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");

// setup the sensors
Adafruit_BME280 bme; // I2C
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;

// icon's bitmap
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) { // on/off the display
display_state = 1 - display_state;
digitalWrite(TFT_Vcc, display_state);
if (display_state == 1) { // turn on the screen
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 to adafruit io
send_data();
}

if (reconnect_cnt >= RECONNECTION_TIME/LOOP_DELAY_TIME) {
reconnect_cnt = 0;
if (!mqtt.connected()) { // reconnect when disconnecting
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); // for 1.8" ST7735 TFT display
tft.setRotation(1); //screen rotation
tft.setTextWrap(false);
tft.fillScreen(ST77XX_BLACK); //filled with black color
}

float get_temperature() {
return bme.readTemperature();
}

float get_humidity() {
return bme.readHumidity();
}

float get_altitude() {
return bme.readAltitude(SEALEVELPRESSURE_HPA);
}

float get_pressure() { // return hPa
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); // return -1 if the value blows up
}

void measure() {
temp = get_temperature();
humidity = get_humidity();
pressure = get_pressure();
altitude = get_altitude();
luminosity = get_luminosity();

// calculate dew point
dewpoint = temp - ((100-humidity)/5.);
// calculate SQM
// SQM = log10f((luminosity/EFFECTIVE_SOLID_ANGLE)/108000)/(-0.4);
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); // set to station mode
WiFi.disconnect(); // init
delay(100);
Serial.println("WiFi setup done");

WiFi.begin(WLAN_SSID, WLAN_PASSWD);
delay(WLAN_TIMEOUT_MS); // important, don't make the delay too short
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("");
}

//publish the data to Adafruit IO
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("");
}

// connect to Adafruit IO
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("");
}

更多功能