项目介绍
本项目基于Seeed的Wio Terminal,在Arduino IDE与VSCode平台开发(C++),可以通过内部的WIFI芯片通过API获取天气信息,并将当天的温湿度、空气质量等信息与三天的天气预报信息显示在LCD屏幕。
另外还加了一些实用花里胡哨的功能:
- 系统初始化阶段设置了一个开屏主页;
- 借助Wio后背的光传感器自动调节屏幕背光亮度;
- 显示RTC时钟并自动校准等。
硬件介绍
Wio Terminal 是基于SAMD51的微控制器,具有Realtek RTL8720DN支持的无线连接,与Arduino和MicroPython兼容。它的运行速度为120MHz(最高可达200MHz),4MB外部闪存和192KB RAM。它同时支持蓝牙和Wi-Fi,为物联网项目提供了骨架。Wio Terminal自身配有2.4寸LCD屏幕, 板载IMU(LIS3DHTR),麦克风,蜂鸣器,microSD卡槽,光传感器和红外发射器(IR 940nm)。 最重要的是它还有两个用于Grove生态系统 的多功能Grove端口和40个Raspberry Pi兼容的GPIO引脚,用于支持更多附加组件。
各功能代码及说明
获取天气数据
天气数据的获取使用和风天气的API,可以去官网申请,有开发板(不可商用)与商业版(功能更强及可商用)的区别,商业版的API申请需记次数收费。Wio通过API访问数据并通过ArduinoJson解析,提取需要的信息并存储。
该部分的功能使用类对象实现OW_Weather,写成.h文件,类方法如下,含义顾名思义。
👉 参见Github: ESP8266 and ESP32 OpenWeather Client
class OW_Weather
{
public:
bool getForecast(OW_current *current, OW_forecast *forecast, String locationID, String units, String language, String api_key);
void printCurrentWeather();
void printForecastWeather();
OW_current *current; // pointer provided by sketch to the OW_current struct
OW_forecast *forecast; // pointer provided by sketch to the OW_forecast struct
private:
bool parseRequest(String url, url_type type);
bool parseCurWeatherData(String str);
bool parseForeWeatherData(String str);
bool parseCurPollutionData(String str);
bool parseForePollutionData(String str);
private:
String stream_data;
};
.cpp文件内为各对应方法的函数编写。
WIFI连接
WIFI的连接请参照Arduino及Seeed提供的示例,在此不赘述。
天气信息储存
所需要的天气数据包括(当然可以添加更多):
- 当天的天气、温湿度信息、风力等;
- 当天的空气质量信息;
- 三天的天气预报:天气、温度范围、空气质量预测等;
- 天气信息更新的时间;
- 对应的图标标号等。
这些信息使用结构体存储:
#define FORECAST_DAYS 3
/***************************************************************************************
** Description: Structure for current weather
***************************************************************************************/
typedef struct OW_current
{
String wea_updateTime;
String temp;
String icon;
String text;
String windscale;
String humidity;
/* Air pollution */
String pol_updateTime;
String level;
String category;
String pm10;
String pm2p5;
} OW_current;
/***************************************************************************************
** Description: Structure for forecast weather
***************************************************************************************/
typedef struct OW_forecast
{
String wea_updateTime;
String fxDate[FORECAST_DAYS];
String tempMax[FORECAST_DAYS];
String tempMin[FORECAST_DAYS];
String iconDay[FORECAST_DAYS];
String textDay[FORECAST_DAYS];
String windscale[FORECAST_DAYS];
String humidity[FORECAST_DAYS];
/* Air pollution */
String pol_updateTime;
String aqi[FORECAST_DAYS];
String level[FORECAST_DAYS];
String category[FORECAST_DAYS];
} OW_forecast;
通过API获取数据
根据和风天气API开发文档可以知道获取某些数据需要输入何种格式的网址,例如获取北京空气质量预报(仅商用版API),则格式为:
https://api.qweather.com/v7/air/5d? + location=101010100 + &key=你的KEY + &gzip=n
⚠️ 请注意:最后的&gzip=n,虽然通过浏览器加不加这个无所谓,但是通过Wio访问需要加这个不压缩的限制。
通过上述链接从浏览器获取的信息如下(JSON格式):
{"code":"200","updateTime":"2021-12-29T09:42+08:00","fxLink":"http://hfx.link/2ax4","daily":[{"fxDate":"2021-12-29","aqi":"29","level":"1","category":"优","primary":"NA"},{"fxDate":"2021-12-30","aqi":"38","level":"1","category":"优","primary":"NA"},{"fxDate":"2021-12-31","aqi":"64","level":"2","category":"良","primary":"PM2.5"},{"fxDate":"2022-01-01","aqi":"57","level":"2","category":"良","primary":"PM2.5"},{"fxDate":"2022-01-02","aqi":"58","level":"2","category":"良","primary":"PM2.5"}],"refer":{"sources":["QWeather","CNEMC"],"license":["commercial license"]}}
通过这个url获取数据的部分代码(也可参看Seeed提供的示例):
bool OW_Weather::parseRequest(String url, url_type type)
{
uint32_t dt = millis();
const char* host = "api.qweather.com";
if (!client.connect(host, 443))
{
Serial.println("Connection failed!");
return false;
}
uint32_t timeout = millis();
Serial.print("Sending GET request to "); Serial.print(host); Serial.println("...");
client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");
while (client.available() || client.connected())
{
String line = client.readStringUntil('\n');
if (line == "\r")
{
// Serial.println("Header end found.");
break;
}
if ((millis() - timeout) > 5000UL)
{
Serial.println("HTTP header timeout!");
client.stop();
return false;
}
}
while (client.available())
{
stream_data += client.readStringUntil('\r');
break;
}
bool result;
result = parseForePollutionData(stream_data);
Serial.print("Done in "); Serial.print(millis()-dt); Serial.println(" ms");
stream_data = "";
client.stop();
return result;
}
其中,stream_data为网站发回的数据,为json格式,之后进入parseForePollutionData解析。
JSON解析
解析使用ArduinoJson.h及Assistant,注意你所使用的ArduinoJson的版本。在Assistant v6内,选择“SAMD21”、“Deserialize and filter”、“String”;之后输入网站返回的JSON文本,以及filter(参见Assistant 所提供的 Examples: OpenWeatherMap 看一下你就会写了),最后将所生成的代码copy即可,写成函数包装也可。承接上例,解析JSON数据:
bool OW_Weather::parseForePollutionData(String str)
{
// String input;
StaticJsonDocument<112> filter;
filter["code"] = true;
filter["updateTime"] = true;
JsonObject filter_daily_0 = filter["daily"].createNestedObject();
filter_daily_0["aqi"] = true;
filter_daily_0["level"] = true;
filter_daily_0["category"] = true;
StaticJsonDocument<512> doc;
DeserializationError error = deserializeJson(doc, str, DeserializationOption::Filter(filter));
if (error)
{
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
return false;
}
const char* code = doc["code"]; // "200"
const char* updateTime = doc["updateTime"]; // "2021-12-16T16:42+08:00"
if (strcmp(code, "200") != 0)
{
Serial.println("Air pollution forecast request failed!");
return false;
}
forecast -> pol_updateTime = updateTime;
int i = 0;
for (JsonObject daily_item : doc["daily"].as<JsonArray>())
{
const char* daily_item_aqi = daily_item["aqi"]; // "49", "50", "59", "79", "89"
const char* daily_item_level = daily_item["level"]; // "1", "1", "2", "2", "2"
const char* daily_item_category = daily_item["category"]; // "Excellent", "Excellent", "Good", "Good", ...
if (i != 0)
{
forecast -> aqi[i-1] = daily_item_aqi;
forecast -> level[i-1] = daily_item_level;
forecast -> category[i-1] = daily_item_category;
}
if (i == FORECAST_DAYS)
break;
else
i++;
}
return true;
}
至此,将网站返回的7天空气质量预测,取其中3天的AQI、Level、Category、updateTime信息存储至结构体,之后访问该类结构体即可访问数据。
解析后的数据包括图标编号一项,其图标下载及使用方式参见:
由于和风天气的图标格式为svg,转成能显示在Wio的8/16位BMP属实复杂,且其大小仅为16x16,显示效果不佳,因此只能换用其他的天气图标(大小32x32,显示效果好):
但是这些图片的含义与和风天气也有所不同,因此只能选取部分天气显示图标,且仅使用白天的天气图标,并手动构建一个对应表(当然这不是最佳方案):
const char* icon_table[20][2] =
{
{"100", "wi-day-sunny"},
{"101", "wi-cloudy"},
{"102", "wi-cloud"},
{"103", "wi-day-cloudy"},
{"104", "wi-cloud"},
{"150", "wi-night-clear"},
{"154", "wi-cloud"},
{"305", "wi-rain"},
{"306", "wi-rain"},
{"307", "wi-rain"},
{"399", "wi-rain"},
{"400", "wi-snow"},
{"401", "wi-snow"},
{"402", "wi-snow"},
{"404", "wi-sleet"},
{"409", "wi-snow"},
{"501", "wi-fog"},
{"502", "wi-hail"},
{"504", "wi-dust"},
{"507", "wi-sandstorm"}
};
上述代码,将获取的天气编号(自和风天气)转成新找的天气对应的图标编号,有些天气无法对应(例如小雪、中雪、大雪,但在新找的天气图标内没有更细分的因此可合并显示成一种图标),可简化显示。
查表获取天气图标名称并显示,图标会显示在当天天气信息右下角。
char* icon_pic_name = new char[40];
int i = 0;
for (i = 0; i <= 19; i++)
{
if (strcmp(icon_table[i][0], (current->icon).c_str()) == 0)
break;
if (i == 19)
{
Serial.println("No icon found!");
return false;
}
}
icon_pic_name = strcpy(icon_pic_name, icon_table[i][1]);
icon_pic_name = strcat(icon_pic_name, ".bmp");
drawImage<uint16_t>(icon_pic_name, 320-32, 240-32);
函数调用
在初始化中(虽然Arduino非要写成setup()、与loop()),可以由如下方式调用:
OW_Weather ow; // Weather forecast library instance
OW_current *current = new OW_current;
OW_forecast *forecast = new OW_forecast;
Serial.println("Requesting weather information from QWeather...");
if (ow.getForecast(current, forecast, locationID, units, language, com_key) == true)
{
ow.printCurrentWeather();
ow.printForecastWeather();
}
其中,locationID、units、language的含义参见和风天气开发文档,可以通过宏定义等方式声明。com_key为你申请的API Key。
if内的两个print只是单纯的访问类结构体的变量通过串口打印方便debug,代码略。
而后,笔者设置按下按键(Key B)以手动更新一次天气数据:
if (digitalRead(WIO_KEY_C) == LOW)
{
ow.getForecast(current, forecast, locationID, units, language, com_key);
ow.printCurrentWeather();
ow.printForecastWeather();
}
RTC时钟
👉 参见Wio RTC
使用NTP对RTC时钟进行校准,但没有上操作系统,因此时钟误差很大,而且访问NTP时常失败。该部分代码略。
LCD显示
Wio的显示屏外观、质量真是OK。对LCD屏的基本操作:
👉 参见Wio LCD
此外,为了丰富显示效果,加入了几种smooth font及图片(需要TF卡),参见上述链接。
LCD显示效果的设计较为自由,按照上述链接设计的效果都比笔者的好看。此外,由于设计的页面无法一次性显示当天+三天预报,因此设置一个假页面切换功能(实际就是页面刷新显示不同内容),通过五向开关左右按键实现。其效果如图所示,右下角为天气图标,空气质量的颜色与等级对应。
可以发现“切换页面”的时候,下方黑点随之切换,以实现页面切换效果。其核心代码如下:
bool PageDisplay(int current_page)
{
int xpos, ypos;
xpos = tft_lcd.width() / 2;
ypos = tft_lcd.height() - 15;
tft_lcd.setTextDatum(MC_DATUM);
if (current_page == 1)
{
tft_lcd.fillCircle(xpos - 6, ypos, 4, TFT_BLACK);
tft_lcd.drawCircle(xpos + 6, ypos, 4, TFT_BLACK);
}
else if (current_page == 2)
{
tft_lcd.drawCircle(xpos - 6, ypos, 4, TFT_BLACK);
tft_lcd.fillCircle(xpos + 6, ypos, 4, TFT_BLACK);
}
else
return false;
return true;
}
通过设置一个全局变量current_page记录,之后分别显示当天及三天天气信息时需要加上一句:
tft_lcd.fillRect(0, 50, 320, 215, BACKGROUND_COLOR);
将之前显示的内容填背景色方块以实现刷新效果。
自动调节LCD背光
根据Wio后背光传感器读取的数值调节背光亮度:
👉 参见Wio Light Sensor
👉 参见Controlling Brightness of the LCD Backlight(网页拉到底)
代码如下:
void setup()
{
...
pinMode(WIO_LIGHT, INPUT);
...
}
/* 背光自动调节功能函数 */
uint8_t BacklightAdjust(int light_value)
{
uint8_t cur_brightness;
if (light_value <= 150)
{
cur_brightness = (uint8_t)15;
}
else if (light_value <= 300)
{
cur_brightness = (uint8_t)light_value / 10;
}
else if(light_value <= 600)
{
cur_brightness = (uint8_t)light_value / 10 + 10;
}
else
{
cur_brightness = backLight.getMaxBrightness();
}
backLight.setBrightness(cur_brightness);
return cur_brightness;
}
⚠️ 注意:自动调节的效果基本根据经验设置,大致思路是光传感器数值小于150,背光值保持15,当数值大于600,保持最亮100,中间的话按经验尝试设置。(光传感器在LCD屏幕后面在某些场景下效果不理想)
此外,也可以设置一按键,切换手动/自动调节亮度,手动调节通过五向开关的上下实现。
if (blacklight_auto_adjust)
{
cur_brightness = BacklightAdjust(analogRead(WIO_LIGHT));
}
else
{
if (digitalRead(WIO_5S_UP) == LOW)
{
cur_brightness = cur_brightness + 10;
}
if (digitalRead(WIO_5S_DOWN) == LOW)
{
cur_brightness = cur_brightness - 10;
}
backLight.setBrightness(cur_brightness);
}
blacklight_auto_adjust全局变量记录亮度调节的设置(Manual or Auto),cur_brightness为此时的背光亮度值。
初始化阶段屏幕主页整活
在工程初始化阶段,需要首先进行GPIO、WIFI、Serial、LCD的初始化,并联网获取RTC、天气信息等数据,耗时10秒左右,在此期间,LCD闲着没事干,因此显示一下头像、作者信息、提示“Initializing”的信息。
主要功能函数(显示图片与smooth font需要预先在TF卡内存好并插入TF卡至Wio,链接见前):
void LCD_Init(uint16_t background_color)
{
tft_lcd.begin();
tft_lcd.setRotation(1);
tft_lcd.fillScreen(background_color); //Black background
backLight.initialize();
drawImage<uint16_t>("logo_128x128.bmp", 160-128/2, 120-128/2-20);
tft_lcd.setTextDatum(MC_DATUM);
tft_lcd.setTextColor(TFT_BLACK, BACKGROUND_COLOR);
tft_lcd.loadFont("CourierNewPS-ItalicMT-16");
tft_lcd.drawString("By KafCoppelia", 160, 190);
}
void InitializingDisplay(void)
{
tft_lcd.loadFont("TimesNewRomanPSMT-24");
tft_lcd.drawString("Initializing...", 160, 220);
tft_lcd.unloadFont();
}
在setup()阶段调用这两个即可。效果如图所示:
功能展示
- 开屏主页显示:见上图,当WIFI连接成功后,主页头像下会出现“Initializing”,表示正在获取天气信息,后续将WIFI的连接改为更智能一些(以适应多地wifi的切换,例如家与公司),且显示WIFI连接成功与否;
- 两个天气信息的展示及切换,图见上,直男UI设计;
- RTC时钟展示见上;
- 自动调节背光亮度及更多见下方演示视频。
👉 项目演示视频参见上方。
总结与吐槽
此次项目实现了:
- 当天天气信息(天气、温湿度、空气质量、风力、更新时间)与三天天气预报(天气、温度范围、空气质量、更新时间)的获取与更新;
- RTC时钟(虽然不是很准)的校准与显示;
- 一些杂七杂八的显示效果;
- LCD背光亮度的自动调节。
Wio Terminal 内置的WIFI与蓝牙芯片使得该开发板可以简便地运用于IOT中,正面的LCD屏幕显示彩图效果很上眼,要是背部有个磁吸能吸在电脑主机或是书架上那将绝杀。Wio的背部还有40Pin,其拓展能力在此次项目中还没用上,可以说没用到Wio十分之一的功力。
此次项目也让我新接触到了Arduino IDE的开发环境及生态。BTW,Arduino IDE的开发环境是真的差劲,.h文件找半天;一些示例也没说明白,Seeed官网上的相关帮助网页还没中文,属实有点难顶。