Funpack第12期 Wio Terminal 网络天气预报仪
本项目使用Wio Terminal制作了一个网络天气预报仪,可以连接WiFi后通过HTTP GET获取OpenWeatherMap提供的天气API数据并显示在LCD上
标签
嵌入式系统
常勃兄
更新2021-12-24
厦门大学
735

实现功能:

本项目使用了Arduino IDE进行开发,使用Wio Terminal的WiFi模块连接网络,定时向OpenWeatherMap的OneCall API与Air Pollution API发送HTTP GET请求,获取API发送的Json格式数据并解析,得到当前的天气情况、温度湿度气压;明天、后天、大后天中午的天气情况与温度;当前空气质量指数与各项空气污染物浓度等信息,并将这些信息分为当前天气、空气质量、3天天气预报共3个屏幕的内容轮流显示在屏幕上。

程序在白天时屏幕的背景色与文字颜色会根据天气变化,且可以根据从API获取的当前时间与今日日出日落时间,在夜间将背景和文字自动切换到深色,同时天气图标也会变为夜间主题的图标。同时对没有成功从网络获取与解析数据的情况做了异常处理。

功能展示:

本节的图中仅展示日间晴天、阴天时的主题与日落后的深色主题,由于拍摄原因,与实际显示效果有所出入。

1.Current界面

         显示当前的天气、温度、湿度、气压信息。

Fhl1OD4gimjoCC47c7PUIdEkYtZC

2.Air Pollution界面

         显示当前空气质量指数、各个空气污染物的浓度。

Fop6EfVBnqV9V3NMSozuVx9B2vXR

3.3 Days Forecast界面

         显示未来3天中午13:00时的天气情况与温度。

FqZLKHMSKe1WpTwwphotlcTmsGRL

重要代码片段:

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&&current_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活动让我有机会有动力上手本开发板,感谢电子森林让我认识了不少牛逼的大佬,能和大佬们在群里吹水非常荣幸,哈哈哈。

附件下载
Weather_Get_and_Display.zip
团队介绍
就我一个人哦
团队成员
常勃兄
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号