实现功能:
本项目使用了Arduino IDE进行开发,使用Wio Terminal的WiFi模块连接网络,定时向OpenWeatherMap的OneCall API与Air Pollution API发送HTTP GET请求,获取API发送的Json格式数据并解析,得到当前的天气情况、温度湿度气压;明天、后天、大后天中午的天气情况与温度;当前空气质量指数与各项空气污染物浓度等信息,并将这些信息分为当前天气、空气质量、3天天气预报共3个屏幕的内容轮流显示在屏幕上。
程序在白天时屏幕的背景色与文字颜色会根据天气变化,且可以根据从API获取的当前时间与今日日出日落时间,在夜间将背景和文字自动切换到深色,同时天气图标也会变为夜间主题的图标。同时对没有成功从网络获取与解析数据的情况做了异常处理。
功能展示:
本节的图中仅展示日间晴天、阴天时的主题与日落后的深色主题,由于拍摄原因,与实际显示效果有所出入。
1.Current界面
显示当前的天气、温度、湿度、气压信息。
2.Air Pollution界面
显示当前空气质量指数、各个空气污染物的浓度。
3.3 Days Forecast界面
显示未来3天中午13:00时的天气情况与温度。
重要代码片段:
1.功能划分
本项目需要使用MCU做的工作可以分为:WiFi连接与通讯、Json代码解析、屏幕显示3个部分,为了让代码简洁易修改,我将这三个部分的工作作为多个函数写在了3个.h文件内,在.ino文件内#include它们,然后通过调用需要使用的函数来将3个部分连接起来,这样.ino文件内就只要完成初始化工作、在划分好的时间内循环执行三个部分的工作即可。
//用到的头文件
#include "config_debug.h"
#include "WiFi_and_Network.h"
#include "JSON_Deserialize.h"
#include "TFT_Draw.h"
//定义用于计时的全局变量
static unsigned long start_time_stamp;
const int period=121000;
const int screen_time=6000;
static bool info_done=0;
static bool scr0_done=0;
static bool scr1_done=0;
static bool scr2_done=0;
//各模块初始化
void setup() {
Serial.begin(115200);
while (!SD.begin(SDCARD_SS_PIN, SDCARD_SPI));
String IPAdress=WiFi_Connect(WIFI_SSID,WIFI_PSWD);
initial_and_infoscreen(IPAdress);
delay(500);
start_time_stamp=millis();
}
由于本项目使用的Wio Termial的性能充足,初始化完成后,每两分钟花1秒执行一次http get+json解析(实际并不需要这么多时间),接着每2秒刷新一次屏幕,将解析好的数据作为参数调用函数即可绘制屏幕内容。
void loop() {
if((millis()-start_time_stamp)%period>=0&&(millis()-start_time_stamp)%period<1000&&!info_done)//http-get获取天气数据,json解析
{
start_time_stamp=millis()+800;
const String URL_Onecall=String("http://api.openweathermap.org/data/2.5/onecall?lat=")+LAT+"&lon="+LON+"&exclude=minutely,hourly,alerts&appid="+API_KEY+"&units=metric&lang=en";
const String URL_Airqual=String("http://api.openweathermap.org/data/2.5/air_pollution?lat=")+LAT+"&lon="+LON+"&appid="+API_KEY;
String Onecall_result=HTTP_Get(URL_Onecall);
de_aiocall(&all_in_one_call,Onecall_result);
Serial.println(String("One Call http get and deserialize time: ")+(millis()-start_time_stamp));
delay(100);
String Airqual_result=HTTP_Get(URL_Airqual);
de_airquality(&air_quality,Airqual_result);
Serial.println(String("Air Quality http get and deserialize time: ")+(millis()-start_time_stamp));
info_done=1;
delay(100);
}
else
{
info_done=0;
static bool isDay=dayNow(all_in_one_call.current_call_utc,all_in_one_call.current_sunrise_utc,all_in_one_call.current_sunset_utc);
if((((millis()-start_time_stamp)%period)-1000)%screen_time>=0&&(((millis()-start_time_stamp)%period)-1000)%screen_time<2000&&!scr0_done)
{
drawBackground(isDay,all_in_one_call.current_weather_0_id);
draw_Screen0(all_in_one_call.fail,isDay,all_in_one_call.current_weather_0_description,all_in_one_call.current_weather_0_id,all_in_one_call.current_call_utc,all_in_one_call.timezone_offset,all_in_one_call.current_temp,all_in_one_call.current_pressure,all_in_one_call.current_humidity);//current
//delay(1000);
scr0_done=1;
scr1_done=0;
scr2_done=0;
}
if( (((millis()-start_time_stamp)%period)-1000)%screen_time>=2000&&(((millis()-start_time_stamp)%period)-1000)%screen_time<4000&&!scr1_done)
{
drawBackground(isDay,all_in_one_call.current_weather_0_id);
draw_Screen1(air_quality.fail,air_quality.AQI,air_quality.CO,air_quality.NO,air_quality.NO2,air_quality.O3,air_quality.SO2,air_quality.PM2_5,air_quality.PM10,air_quality.NH3);
//delay(1000);
scr0_done=0;
scr1_done=1;
scr2_done=0;
}
if( (((millis()-start_time_stamp)%period)-1000)%screen_time>=4000&&(((millis()-start_time_stamp)%period)-1000)%screen_time<6000&&!scr2_done)
{
drawBackground(isDay,all_in_one_call.current_weather_0_id);
draw_Screen2(all_in_one_call.fail,isDay,all_in_one_call.daily_1_temp_day,all_in_one_call.daily_1_weather_0_description,all_in_one_call.daily_1_weather_0_id,all_in_one_call.daily_2_temp_day,all_in_one_call.daily_2_weather_0_description,all_in_one_call.daily_2_weather_0_id,all_in_one_call.daily_3_temp_day,all_in_one_call.daily_3_weather_0_description,all_in_one_call.daily_3_weather_0_id);
//delay(1000);
scr0_done=0;
scr1_done=0;
scr2_done=1;
}
delay(100);
}
}
注意:由于需要配置WiFi信息与地理位置信息、API密钥(注册OpenWeatherMap即可免费获取)以正常获得天气API的数据,这一部分信息写在了config.h中,需要根据用户的个人使用情况更改config.h才能使固件正常工作。
2.wifi连接与通讯
这一部分的函数负责WiFi的连接与通过网络客户端的http-get方法获取两个API返回的信息。
此部分代码参考https://wiki.seeedstudio.com/Wio-Terminal-Wi-Fi/的代码例程就能搞定。
3.Json解析
这一部分对上一步API返回的Json格式字符串进行解析,并且定义了两个全局结构体变量(C++菜鸡,不知道有没有别的好办法)用于装下解析好的数据以便在下一步中调用函数显示它们。
#include <ArduinoJson.h>
#define ARDUINOJSON_USE_DOUBLE 0
static struct AirQ
{
bool fail;//deserializeJson() failed: 0:failed 1:success
int AQI;
float CO,NO,NO2,O3,SO2,PM2_5,PM10,NH3;
} air_quality;
static struct AioC
{
bool fail;//deserializeJson() failed: 0:failed 1:success
//.......省略一大堆
} all_in_one_call;
Json解析的代码由ArduinoJson库的网站在线生成,我只是把它改成了给以上两个结构体赋值的函数。由于需要解析的数据有些复杂,导致生成的代码也比较繁琐,没有贴上来的必要。
4.屏幕显示
这一部分对Json解析好的数据做简单的分析(UTC时间戳转换、判断是否日落、确定天气主题、确定天气图标)并使用Seeed提供的库函数驱动LCD屏幕显示内容。
//#include <Seeed_Arduino_FS.h>
#include "TFT_eSPI.h"
#include "Seeed_FS.h"
#include "RawImage.h"
#include "UTCConv.h"
#include"Free_Fonts.h"
TFT_eSPI tft;
void initial_and_infoscreen(String IPAdress)
{
tft.begin();
tft.setRotation(3);
tft.fillScreen(0x0000);//black
tft.setTextColor(0xFFFF);
tft.setTextSize(1);
tft.drawString("Connected to the WiFi network with IP: ",0,0);
tft.drawString(IPAdress,0,10);
}
bool dayNow(long current_dt,long sunrise_utc,long sunset_utc)
{
bool day_now;
if(current_dt>=sunrise_utc&¤t_dt<sunset_utc)//day
{
day_now=1;
}
else
{
day_now=0;
}
return day_now;
}
void drawBackground(bool dayNow,int weather_id)//all come from current这些参数决定绘制的背景色
{
if(dayNow)//day
{
if(weather_id==800)//Clear
{
tft.fillScreen(0x867F);//skyblue
tft.setTextColor(TFT_BLACK);
//tft.setTextSize(2);
}
if(weather_id>=801&&weather_id<=804)//Clouds
{
tft.fillScreen(0x8410);//gray
tft.setTextColor(TFT_BLACK);
//tft.setTextSize(2);
}
if(weather_id>=300&&weather_id<=781)//rain snow thunder fog
{
tft.fillScreen(0x4208);//darker_gray
tft.setTextColor(TFT_WHITE);
//tft.setTextSize(2);
}
}
else//night
{
//tft.fillScreen(0x1043);//darkest_gray
tft.fillScreen(TFT_BLACK);
tft.setTextColor(0x8410);
//tft.setTextSize(2);
}
}
void drawWeatherIcon(bool dayNow,int weather_id,int posX,int posY)
{
if(weather_id>=200&&weather_id<=232)
{
drawImage<uint8_t>("200.bmp", posX, posY);
}
if(weather_id>=300&&weather_id<=321)
{
drawImage<uint8_t>("300.bmp", posX, posY);
}
if(weather_id>=500&&weather_id<=531)
{
drawImage<uint8_t>("500.bmp", posX, posY);
}
if(weather_id>=600&&weather_id<=622)
{
drawImage<uint8_t>("600.bmp", posX, posY);
}
if(weather_id>=701&&weather_id<=781)
{
drawImage<uint8_t>("701.bmp", posX, posY);
}
if(weather_id==800)
{
if(dayNow)
{
drawImage<uint8_t>("8000.bmp", posX, posY);
}
else
{
drawImage<uint8_t>("8001.bmp", posX, posY);
}
}
if(weather_id==801)
{
if(dayNow)
{
drawImage<uint8_t>("8010.bmp", posX, posY);
}
else
{
drawImage<uint8_t>("8011.bmp", posX, posY);
}
}
if(weather_id>=802&&weather_id<=804)
{
drawImage<uint8_t>("802.bmp", posX, posY);
}
}
void draw_Screen0(bool fail,bool dayNow,const char *weather,int weather_id,long utc_dt,int timezone_offset,float temp,int pressure,int humidity)//current
{
if(fail)
{
tft.fillRect(0, 152, 320, 88, TFT_WHITE);
drawWeatherIcon(dayNow,weather_id,128,152);
tft.setFreeFont(FSS12);
tft.drawString("Current Weather: ",0,32);
tft.setFreeFont(FSS18);
tft.drawString(weather,0,54);
tft.setFreeFont(FSS12);
tft.drawString(String("temperature: ")+temp+"'C",0,87);
tft.drawString(String("humidity: ")+humidity+"%",0,109);
tft.drawString(String("pressure: ")+pressure+"hPa",0,131);
}
else
{
tft.setFreeFont(FSS12);
tft.drawString("data get failed",80,100);
}
tft.setFreeFont(FSS18);
tft.setTextColor(TFT_ORANGE);
tft.drawString("Current",100,0);
tft.drawFastHLine(0, 30, 320, TFT_ORANGE);
if(fail)
{
tft.setFreeFont(FSS9);
tft.drawString(utc_conv(utc_dt,timezone_offset),170,220);
}
}
void draw_Screen1(bool fail,int AQI,float CO,float NO,float NO2,float O3,float SO2,float PM2_5,float PM10,float NH3)//air quality
{
if(fail)
{
tft.setFreeFont(FSS12);
tft.drawString("Air Quality Index:",5,32);
tft.setFreeFont(FSS18);
tft.drawString(String(AQI),5,54);
switch(AQI)
{
case 1:
{
tft.drawString(" Good",25,57);
break;
}
case 2:
{
tft.drawString(" Fair",25,57);
break;
}
case 3:
{
tft.drawString(" Moderate",25,57);
break;
}
case 4:
{
tft.drawString(" Poor",25,57);
break;
}
case 5:
{
tft.drawString(" Very Poor",25,57);
break;
}
}
tft.setFreeFont(FSS12);
tft.drawString(String("CO: ")+CO+"ug/m3",5,87);
tft.drawString(String("NO: ")+NO+"ug/m3",5,109);
tft.drawString(String("NO2: ")+NO2+"ug/m3",5,131);
tft.drawString(String("O3: ")+O3+"ug/m3",5,153);
tft.drawString(String("SO2: ")+SO2+"ug/m3",5,175);
tft.drawString(String("PM2.5: ")+PM2_5+"ug/m3",5,197);
tft.drawString(String("PM10: ")+PM10+"ug/m3",5,219);
//tft.drawString(String("NH3: ")+NH3+"ug/m3",5,241);
}
else
{
tft.setFreeFont(FSS12);
tft.drawString("data get failed",80,100);
}
tft.setTextColor(TFT_ORANGE);
tft.setFreeFont(FSS18);
tft.drawString("Air Pollution",70,0);
tft.drawFastHLine(0, 30, 320, TFT_ORANGE);
//tft.drawString(utc_conv(utc_dt,timezone_offset),0,15);
}
void draw_Screen2(bool fail,bool dayNow,float daily_1_temp,const char *daily_1_description,int daily_1_weather_id,float daily_2_temp,const char *daily_2_description,int daily_2_weather_id,float daily_3_temp,const char *daily_3_description,int daily_3_weather_id)//forecast
{
if(fail)
{
tft.fillRect(0, 0, 60, 240, TFT_WHITE);
tft.setFreeFont(FSS12);
drawWeatherIcon(dayNow,daily_1_weather_id,-4,8);
tft.drawString(String("next day:")+daily_1_description,64,22);
tft.drawString(String("temperature:")+daily_1_temp+"'C",64,43);
drawWeatherIcon(dayNow,daily_2_weather_id,-4,88);
tft.drawString(String("2nd day:")+daily_2_description,64,102);
tft.drawString(String("temperature:")+daily_2_temp+"'C",64,123);
drawWeatherIcon(dayNow,daily_3_weather_id,-4,168);
tft.drawString(String("3rd day:")+daily_3_description,64,182);
tft.drawString(String("temperature:")+daily_3_temp+"'C",64,203);
tft.drawFastHLine(0, 80, 320, TFT_ORANGE);
tft.drawFastHLine(0, 160, 320, TFT_ORANGE);
}
else
{
tft.setFreeFont(FSS12);
tft.drawString("data get failed",80,100);
}
tft.setTextColor(TFT_ORANGE);
tft.setFreeFont(FSS12);
tft.drawString("3 Days Forecast",80,0);
//tft.drawString(utc_conv(utc_dt,timezone_offset),0,15);
}
由于从API获取的时间是UTC时间戳,为了显示更新时间,这里需要使用C++自带的time.h将UTC时间戳转换为我们熟悉的年月日,时:分:秒,另外为了显示彩色图像,我们需要将24位RGB真彩色图像变成RGB332的8位色图,然后将图像放在SD卡中,读取SD卡、按图像的坐标发送给屏幕来显示彩色图像。Seeed非常贴心的为图像转换写了一个简单的python脚本,只需要将图像放在命名为bmp的文件夹内,然后将python脚本放在与bmp文件夹同一目录下,执行python脚本就能完成图像的转换。
心得体会:
本次项目是我第一次开发物联网应用,之前对此一直想要了解,为了完成本项目,我学习了IOT的一些知识,打开了物联网的大门,以后可能会使用成本更低的硬件尝试开发更多IoT项目。
由于没有看懂NTP的示例,本次项目没有做NTP,有些遗憾,希望以后弄懂了能加上NTP功能吧……
本次项目算是我做的比较复杂的Arduino项目了,通过本次项目,深切感受到自己代码基础过于薄弱,由于C++面向对象掌握的太差,有大量多种类的数据时只能使用结构体传递参数,有些浪费RAM空间,幸好本次使用的开发板性能足够强大,否则真的可能难以完成……
最后非常感谢FunPack活动让我有机会有动力上手本开发板,感谢电子森林让我认识了不少牛逼的大佬,能和大佬们在群里吹水非常荣幸,哈哈哈。