这段时间在做“硬禾学堂”推出的寒假一起练活动,只要完成了对应的项目就可以免费获得开发板。
下面是我本次完成项目的一些分享:
项目介绍
我完成的项目是 制作一个本地气象台/温度计
-
利用OLED显示
-
显示当前本地的时间、温度和气象信息
硬件介绍
使用的开发板搭载的是乐鑫ESP32-S2-mini-1模块,配备了Xtensa单核32位LX7微处理器,支持高达240MHz的时钟频率以及4MB嵌入式flash,功能很强大。
本次项目使用到了OLED显示屏,WIFI以及按键三个模块。
本次我使用的平台是arduino。
按键操作
本次我只使用了一个按键用于切换显示界面,一共设置了三个显示界面分别是时间、天气、IP定位。
功能展示
上电会自动连接给定的WiFi,连接成功后会自动获取网络时间并进行显示
按一下最左边的按键后切换到天气显示界面,显示当天的气温和天气
再次按下最左边的按键后切换到IP定位显示界面,显示当前网络的IP地址以及定位信息
代码分析
首先是WiFi连接部分,调用WiFi库,定义需要连接的wifi名称和wifi密码,启用WiFi.begin();来连接WiFi。
#include <WiFi.h>//调用WiFi库
const char* ssid="weather";//WiFi名称
const char* password="88888888";//WiFi密码
WiFi.begin(ssid, password);//连接WiFi
获取外网IP地址
引入HTTPClient库,通过http://ipinfo.io/ip这个链接来获取外网IP,刚开始我使用的是WiFi库里面的WiFi.localIP();来获取IP发现获取到的是局域网的IP,并不能用来定位,后来去查找了好多文档找到了这个方法。
#include <HTTPClient.h>
HTTPClient http;
//存放ip地址
String pageData = "";
//获取IP地址
void GetIP()
{
http.begin("http://ipinfo.io/ip");
int httpCode = http.GET();
if(httpCode == HTTP_CODE_OK){
pageData = http .getString();
Serial.print(pageData);
ip = pageData;
}else{
Serial.println("GET ERR");
}
http.end();
}
获取时间
获取网络时间服务器最常用的主机名是 pool.ntp.org;
通过网络时间服务器获得的时间是世界协调时间(UTC)/格林尼治时间(GMT),不同地区的时间可以通过时区换算, gmtOffset_sec 参数就是用来修正时区的,对于我们东八区(UTC/GMT+08:00)来说该参数需要填写 8 * 3600 ;
如果使用夏令时 daylightOffset_sec 就填写3600,否则就填写0;
最后的输出部分:%F 年-月-日,%T 显示时分秒:hh:mm:ss,%A 星期几的全称。
//时间部分
const char *ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 8 * 3600;
const int daylightOffset_sec = 0;
struct tm timeinfo;
void printLocalTime()
{
if (!getLocalTime(&timeinfo))
{
Serial.println("Failed to obtain time");
return;
}
Serial.println(&timeinfo, "%F %T %A");//格式化输出时间信息
}
根据IP获取定位
在IP定位中我使用的是高德地图提供的API接口,获取到的数据是Json格式所以要引入Json库,在使用前需要到高德地图申请个人认证开发者,创建自己的个人密匙后参考他给出的接口(https://restapi.amap.com/v3/ip?ip=用户的IP&output=xml&key=<用户的key>)利用GET请求获取到定位。
#include <ArduinoJson.h>
//获取定位部分,使用的是高德地图
const char *host_ip = "restapi.amap.com";
const char *privateKey_ip = "********";//输入申请到的密匙
String ip;
//存放城市定位
struct IPData
{
char city_ip[32];
char province_ip[32];
};
struct IPData ipdata = {0};
//获取定位信息
void get_gps()
{
WiFiClient client;
if (!client.connect(host_ip, 80))
{
Serial.println("Connect host failed!");
return;
}
Serial.println("host Conected!");
String getUrl = "/v3/ip?ip=";
getUrl += ip;
getUrl += "&key=";
getUrl += privateKey_ip;
getUrl += "&coor=bd09ll";
client.print(String("GET ") + getUrl + " HTTP/1.1
" + "Host: " + host_ip + "
" + "Connection: close
");
Serial.println("Get send");
char endOfHeaders[] = "
";
bool ok = client.find(endOfHeaders);
if (!ok)
{
Serial.println("No response or invalid response!");
}
Serial.println("Skip headers");
String line="";
line += client.readStringUntil('
');
Serial.println(line);
DynamicJsonDocument doc(1400);
DeserializationError error = deserializeJson(doc, line);
if (error)
{
Serial.println("deserialize json failed");
return;
}
Serial.println("deserialize json success");
strcpy(ipdata.city_ip, doc["city"].as<const char*>());
strcpy(ipdata.province_ip, doc["province"].as<const char*>());
// 向串口发送城市信息,调试用
// Serial.print("市:");
// Serial.println(ipdata.city_ip);
// Serial.print("省:");
// Serial.println(ipdata.province_ip);
city = ipdata.city_ip;
province = ipdata.province_ip;
client.stop();
}
获取天气信息
在查询到IP并获取到定位后可以去查询天气信息了,我使用的是心知天气提供的API接口,同样也需要去申请一个并获取密匙,他有查询频率的限制(20次/分钟),查阅官网给出的文档,并根据他给的接口(https://api.seniverse.com/v3/weather/now.json?key=your_private_key&location=beijing&language=zh-Hans&unit=c)利用GET请求去查询天气。
//天气部分,使用的是心知天气
const char *host_weather = "api.seniverse.com";
const char *privateKey_weather = "******";//输入申请到的密匙
String city = "";
String province = "";
const char *language = "zh-Hans";
//存放天气信息
struct WetherData
{
char city[32];
char weather[64];
char high[32];
char low[32];
char humi[32];
};
struct WetherData weatherdata = {0};
//获取天气信息
void get_weather()
{
WiFiClient client;
if (!client.connect(host_weather, 80))
{
Serial.println("Connect host failed!");
return;
}
Serial.println("host Conected!");
String getUrl = "/v3/weather/daily.json?key=";
getUrl += privateKey_weather;
getUrl += "&location=";
getUrl += city;
getUrl += "&language=";
getUrl += language;
client.print(String("GET ") + getUrl + " HTTP/1.1
" + "Host: " + host_weather + "
" + "Connection: close
");
Serial.println("Get send");
char endOfHeaders[] = "
";
bool ok = client.find(endOfHeaders);
if (!ok)
{
Serial.println("No response or invalid response!");
}
Serial.println("Skip headers");
String line="";
line += client.readStringUntil('
');
Serial.println(line);
DynamicJsonDocument doc(1400);
DeserializationError error = deserializeJson(doc, line);
if (error)
{
Serial.println("deserialize json failed");
return;
}
// Serial.println("deserialize json success");
strcpy(weatherdata.city, doc["results"][0]["location"]["name"].as<const char*>());
strcpy(weatherdata.weather, doc["results"][0]["daily"][0]["text_day"].as<const char*>());
strcpy(weatherdata.high, doc["results"][0]["daily"][0]["high"].as<const char*>());
strcpy(weatherdata.low, doc["results"][0]["daily"][0]["low"].as<const char*>());
strcpy(weatherdata.humi, doc["results"][0]["daily"][0]["humidity"].as<const char*>());
// 向串口发送天气信息,调试用
// Serial.print("城市:");
// Serial.println(weatherdata.city);
// Serial.print("天气状况:");
// Serial.println(weatherdata.weather);
// Serial.print("最高气温:");
// Serial.println(weatherdata.high);
// Serial.print("最低气温:");
// Serial.println(weatherdata.low);
// Serial.print("湿度:");
// Serial.println(weatherdata.humi);
//
// Serial.println("read json success");
// Serial.println();
// Serial.println("closing connection");
client.stop();
}
屏幕使用
该项目中我使用的是U8g2的屏幕库,屏幕是SSD1306,并对相应的管脚进行设置,我使用的字体是chinese2,刚开始使用的时候发现好多内容都不显示,查阅了资料后发现是库里面没有包含这些文字,按照教程把我需要使用的字都添加进去后就显示正常了。
#include <U8g2lib.h>//调用屏幕显示函数库
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 36, /* data=*/ 35, /* cs=*/ 46, /* dc=*/ 33, /* reset=*/ 34);//设置屏幕
u8g2.begin(); //开始使用屏幕驱动
u8g2.enableUTF8Print();//启用UTF8支持函数(Unicode的一种可变长度字符编码函数)
u8g2.setFont(u8g2_font_unifont_t_chinese2); //设置显示中文字体2
//u8g2.setFontDirection(0);//设置字体方向
u8g2.firstPage();//第一页
do {
//显示天气状况
if(show_mode == 2)
{
sprintf(str,"城市: %s",weatherdata.city);
u8g2.setCursor(10, 15);//设置光标行与列
u8g2.print(str); //显示城市
sprintf(str,"温度: %s~%s",weatherdata.low,weatherdata.high);
u8g2.setCursor(10, 32);//设置光标行与列
u8g2.print(str);//显示温度
sprintf(str,"天气: %s",weatherdata.weather);
u8g2.setCursor(10, 49);//设置光标行与列
u8g2.print(str); //显示天气
}
//显示时间信息
if(show_mode == 1)
{
u8g2.setCursor(10, 15);//设置光标行与列
u8g2.print(&timeinfo, "%F"); //显示日期
u8g2.setCursor(10, 32);//设置光标行与列
u8g2.print(&timeinfo, "%T");//显示时间
u8g2.setCursor(10, 49);//设置光标行与列
u8g2.print(&timeinfo, "%A"); //显示星期
}
if(show_mode == 3)
{
u8g2.setCursor(0, 15);//设置光标行与列
u8g2.print(ip); //显示ip地址
sprintf(str,"省: %s",province);
u8g2.setCursor(0, 32);//设置光标行与列
u8g2.print(str); //显示省
sprintf(str,"市: %s",city);
u8g2.setCursor(0, 49);//设置光标行与列
u8g2.print(str);//显示市
}
} while ( u8g2.nextPage());//循环下一页
完整代码展示
其中我把天气、IP、定位的刷新频率做了设置,一分钟刷新一次。
#include <Arduino.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <WiFi.h>//调用WiFi库
#include <U8g2lib.h>//调用屏幕显示函数库
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 36, /* data=*/ 35, /* cs=*/ 46, /* dc=*/ 33, /* reset=*/ 34);//设置屏幕
const char* ssid="weather";//WiFi名称
const char* password="88888888";//WiFi密码
HTTPClient http;
//存放ip地址
String pageData = "";
//天气部分,使用的是心知天气
const char *host_weather = "api.seniverse.com";
const char *privateKey_weather = "******";//输入申请到的密匙
//char *city = NULL;
String city = "";
String province = "";
const char *language = "zh-Hans";
//获取定位部分,使用的是高德地图
const char *host_ip = "restapi.amap.com";
const char *privateKey_ip = "******";//输入申请到的密匙
//String *ip = NULL;
String ip;
//时间部分
const char *ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 8 * 3600;
const int daylightOffset_sec = 0;
struct tm timeinfo;
//按键部分
int button1Pin = 1;
int button2Pin = 2;
int button3Pin = 3;
int button4Pin = 6;
//屏幕显示状态
int show_mode = 1;
//屏幕显示
char str[64] = {0};
//天气刷新时间存放
unsigned long get_time = 0;
//存放天气信息
struct WetherData
{
char city[32];
char weather[64];
char high[32];
char low[32];
char humi[32];
};
//存放城市定位
struct IPData
{
char city_ip[32];
char province_ip[32];
};
struct WetherData weatherdata = {0};
struct IPData ipdata = {0};
//获取IP地址
void GetIP()
{
http.begin("http://ipinfo.io/ip");
int httpCode = http.GET();
if(httpCode == HTTP_CODE_OK){
pageData = http .getString();
Serial.print(pageData);
ip = pageData;
}else{
Serial.println("GET ERR");
}
http.end();
}
//获取定位信息
void get_gps()
{
WiFiClient client;
if (!client.connect(host_ip, 80))
{
Serial.println("Connect host failed!");
return;
}
Serial.println("host Conected!");
String getUrl = "/v3/ip?ip=";
getUrl += ip;
getUrl += "&key=";
getUrl += privateKey_ip;
getUrl += "&coor=bd09ll";
client.print(String("GET ") + getUrl + " HTTP/1.1
" + "Host: " + host_ip + "
" + "Connection: close
");
Serial.println("Get send");
char endOfHeaders[] = "
";
bool ok = client.find(endOfHeaders);
if (!ok)
{
Serial.println("No response or invalid response!");
}
Serial.println("Skip headers");
String line="";
line += client.readStringUntil('
');
Serial.println(line);
DynamicJsonDocument doc(1400);
DeserializationError error = deserializeJson(doc, line);
if (error)
{
Serial.println("deserialize json failed");
return;
}
Serial.println("deserialize json success");
strcpy(ipdata.city_ip, doc["city"].as<const char*>());
strcpy(ipdata.province_ip, doc["province"].as<const char*>());
// 向串口发送城市信息,调试用
// Serial.print("市:");
// Serial.println(ipdata.city_ip);
// Serial.print("省:");
// Serial.println(ipdata.province_ip);
city = ipdata.city_ip;
province = ipdata.province_ip;
client.stop();
}
void printLocalTime()
{
if (!getLocalTime(&timeinfo))
{
Serial.println("Failed to obtain time");
return;
}
Serial.println(&timeinfo, "%F %T %A");//格式化输出时间信息
}
//获取天气信息
void get_weather()
{
WiFiClient client;
if (!client.connect(host_weather, 80))
{
Serial.println("Connect host failed!");
return;
}
Serial.println("host Conected!");
String getUrl = "/v3/weather/daily.json?key=";
getUrl += privateKey_weather;
getUrl += "&location=";
getUrl += city;
getUrl += "&language=";
getUrl += language;
client.print(String("GET ") + getUrl + " HTTP/1.1
" + "Host: " + host_weather + "
" + "Connection: close
");
Serial.println("Get send");
char endOfHeaders[] = "
";
bool ok = client.find(endOfHeaders);
if (!ok)
{
Serial.println("No response or invalid response!");
}
Serial.println("Skip headers");
String line="";
line += client.readStringUntil('
');
Serial.println(line);
DynamicJsonDocument doc(1400);
DeserializationError error = deserializeJson(doc, line);
if (error)
{
Serial.println("deserialize json failed");
return;
}
// Serial.println("deserialize json success");
strcpy(weatherdata.city, doc["results"][0]["location"]["name"].as<const char*>());
strcpy(weatherdata.weather, doc["results"][0]["daily"][0]["text_day"].as<const char*>());
strcpy(weatherdata.high, doc["results"][0]["daily"][0]["high"].as<const char*>());
strcpy(weatherdata.low, doc["results"][0]["daily"][0]["low"].as<const char*>());
strcpy(weatherdata.humi, doc["results"][0]["daily"][0]["humidity"].as<const char*>());
// 向串口发送天气信息,调试用
// Serial.print("城市:");
// Serial.println(weatherdata.city);
// Serial.print("天气状况:");
// Serial.println(weatherdata.weather);
// Serial.print("最高气温:");
// Serial.println(weatherdata.high);
// Serial.print("最低气温:");
// Serial.println(weatherdata.low);
// Serial.print("湿度:");
// Serial.println(weatherdata.humi);
//
// Serial.println("read json success");
// Serial.println();
// Serial.println("closing connection");
client.stop();
}
void setup()
{
// put your setup code here, to run once:
u8g2.begin(); //开始使用屏幕驱动
u8g2.enableUTF8Print();//启用UTF8支持函数(Unicode的一种可变长度字符编码函数)
Serial.begin(115200);
//按键初始化
pinMode(button1Pin, INPUT_PULLUP);
pinMode(button2Pin, INPUT_PULLUP);
pinMode(button3Pin, INPUT_PULLUP);
pinMode(button4Pin, INPUT_PULLUP);
Serial.printf("Connecting to %s",ssid);
//Serial.println(ssid);//打印wifi名称
WiFi.begin(ssid, password);//连接WiFi
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
// 打印WiFi连接信息,IP地址,调试用
// Serial.println("");
// Serial.println("WiFi connected");
// Serial.println("IP address: ");
// Serial.println(WiFi.localIP());
//
// Serial.println("");
// Serial.println("WiFi Conected!");
// 从网络时间服务器上获取并设置时间
// 获取成功后芯片会使用RTC时钟保持时间的更新
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
printLocalTime();
GetIP();
get_gps();
get_weather();
}
void loop()
{
// put your main code here, to run repeatedly:
u8g2.setFont(u8g2_font_unifont_t_chinese2); //设置显示中文字体2
//u8g2.setFontDirection(0);//设置字体方向
u8g2.firstPage();//第一页
do {
unsigned long now_time = millis();//获取系统运行时间
//60秒刷新一次气象等信息
if((now_time - get_time) >= 60000)
{
GetIP();
get_gps();
get_weather();
get_time = millis();
}
printLocalTime();
//按键控制部分
if(digitalRead(button1Pin) == LOW)
{
delay(100);
if(digitalRead(button1Pin) == LOW)
{
switch(show_mode)
{
case 1:show_mode = 2;break;
case 2:show_mode = 3;break;
case 3:show_mode = 1;break;
default:break;
}
}
}
//显示天气状况
if(show_mode == 2)
{
sprintf(str,"城市: %s",weatherdata.city);
u8g2.setCursor(10, 15);//设置光标行与列
u8g2.print(str); //显示城市
sprintf(str,"温度: %s~%s",weatherdata.low,weatherdata.high);
u8g2.setCursor(10, 32);//设置光标行与列
u8g2.print(str);//显示温度
sprintf(str,"天气: %s",weatherdata.weather);
u8g2.setCursor(10, 49);//设置光标行与列
u8g2.print(str); //显示天气
}
//显示时间信息
if(show_mode == 1)
{
u8g2.setCursor(10, 15);//设置光标行与列
u8g2.print(&timeinfo, "%F"); //显示日期
u8g2.setCursor(10, 32);//设置光标行与列
u8g2.print(&timeinfo, "%T");//显示时间
u8g2.setCursor(10, 49);//设置光标行与列
u8g2.print(&timeinfo, "%A"); //显示星期
}
if(show_mode == 3)
{
// sprintf(str,"IP:%s",ip);
u8g2.setCursor(0, 15);//设置光标行与列
u8g2.print(ip); //显示ip地址
sprintf(str,"省: %s",province);
u8g2.setCursor(0, 32);//设置光标行与列
u8g2.print(str); //显示省
sprintf(str,"市: %s",city);
u8g2.setCursor(0, 49);//设置光标行与列
u8g2.print(str);//显示市
}
} while ( u8g2.nextPage());//循环下一页
}
存在的问题
1.WiFi需要设置成2.4GHz频段的,设置为5GHz频段的开发板会连接不了。
2.从网络获取时间不一定每次都成功,如果没有成功获取会显示一个默认时间,并且其他功能无法使用,重新上电几次即可解决。
3.手机流量开的热点IP会定位到省会城市,家庭路由器的IP会定位到当前城市,需要使用的话尽量连接家庭路由器的WiFi。