锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

基于Wio Terminal的自动联网的天气预报仪(Arduino/C++)

时间:2023-09-19 22:37:01 wio传感器

目录

    • 项目介绍
    • 硬件介绍
    • 各功能代码及说明
      • 获取天气数据
        • WIFI连接
        • 存储天气信息
        • 通过API获取数据
        • JSON解析
        • 函数调用
      • RTC时钟
      • LCD显示
      • 自动调节LCD背光
      • 初始阶段屏幕主页整活
    • 功能展示
    • 总结与吐槽

项目介绍

本项目基于Seeed的Wio Terminal,在Arduino IDE与VSCode平台开发(C ),可通过内部WIFI芯片通过API获取天气信息,并在三天内显示温湿度、空气质量等信息和天气预报信息LCD屏幕。

此外,还增加了一些实用性花里胡哨的功能:

  1. 在系统初始化阶段设置开屏主页;
  2. 借助Wio背光传感器自动调节屏幕背光亮度;
  3. 显示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支持更多附加组件的引脚。

?? 更多介绍:

  1. Seeed 官网;
  2. 电子森林。
    硬件结构图

各功能代码及说明

获取天气数据

获取和使用天气数据和风天气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这里不重复提供的示例。

存储天气信息

所需要的天数据包括(当然可以添加更多):

  1. 当天的天气、温湿度信息、风力等;
  2. 当天的空气质量信息;
  3. 三天的天气预报:天气、温度范围、空气质量预测等;
  4. 天气信息更新的时间;
  5. 对应的图标标号等。

这些信息使用结构体存储:

#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访问需要加这个不压缩的限制。通过上述链接从浏览器获取的信息如下:

{"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信息存储至结构体,之后访问该类结构体即可访问数据。

解析后的数据包括图标编号一项,其图标下载及使用方式参见:

👉 和风天气图标
👉 Wio Loading Images

由于和风天气的图标格式为svg,转成能显示在Wio的8/16位BMP属实复杂,且其大小仅为16x16,显示效果不佳,因此只能换用其他的天气图标(大小32x32,显示效果好):

👉 Weather Icons

但是这些图片的含义与和风天气也有所不同,因此只能选取部分天气显示图标,且仅使用白天的天气图标,并手动构建一个对应表(当然这不是最佳方案):

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();
}

其中,locationIDunitslanguage的含义参见和风天气开发文档,可以通过宏定义等方式声明。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()阶段调用这两个即可。效果如图所示:

功能展示

  1. 开屏主页显示:见上图,当WIFI连接成功后,主页头像下会出现“Initializing”,表示正在获取天气信息,后续将WIFI的连接改为更智能一些(以适应多地wifi的切换,例如家与公司),且显示WIFI连接成功与否;
  2. 两个天气信息的展示及切换,图见上,直男UI设计

相关文章