本项目为硬禾学堂的2022寒假在家练ESP32-S2物联网平台的项目六,主要的要求为不使用板外资源硬件资源的情况下使用板载的OLED显示屏显示本地时间、温度和气象信息。该项目主要锻炼了物联网系统的开发思路以及大致流程。
由于需要时间、温度和气象等数据,并且无法使用外部硬件资源,因此选择网络进行各种信息的获取。详细思路如下:
由于本项目使用Arduino平台进行开发,因此可以调用丰富的显示驱动库,比如:`U8g2`、`adafruit`等等。本项目采用`U8g2`库进行OLED地驱动。
Arduino中提供了U8g2库的“傻瓜式”下载方式:
在工具中选择工具——选择“管理库”——在界面中搜索“U8g2”后下载安装即可。
安装完毕后,根据OLED的驱动芯片SSD1306以及分辨率128X64选择相应的初始化函数。之后便可根据自身的需要显示出相应的数据即可。
由于需要从网络中获取数据,因此需要使用ESP32S2的WiFi库连接附近WiFi后再调用各种API获取公网IP、位置以及天气等数据。
调用API需要使用HTTPClient库的函数发起HTTP的GET请求,服务器以JSON格式返回请求结果,因此程序中还会涉及JSON格式数据的解析。但Arduino中自带了解析JSON数据的ArduinoJson库,因此可以方便的提取各种请求结果。
但为了提前测试各种API的调用方法以及查看数据格式,可以使用Postman提前测试相应的API。
基本思路是先提取IP,再利用IP进行定位,最后利用位置获取当地天气。
(1)公网IP
(2)位置
(3)天气
ESP32-S2 是一款高度集成、高性价比、低功耗、主打安全的单核 Wi-Fi SoC,具备强大的功能和丰富的 IO 接口。ESP32-S2-MINI-1采用PCB板载天线,模组配置了4MB SPI flash,采用的是 ESP32-S2FN4 芯片。该芯片搭载了Xtensa® 32 位LX7 单核处理器,工作频率高达 240 MHz。用户可以关闭 CPU 的电源,利用低功耗协处理器监测外设的状态变化或某些模拟量是否超出阈值。ESP32-S2-FH4 还集成了丰富的外设接口。
本项目平台在ESP32-S2的基础上又增加了128X64OLED、4个按键、1路Mic音频输入 - 模拟电路、1路耳机插座音频输入 - 模拟电路、2路音频输出、一个FM接收模块和一个模拟开关。
项目使用了 1043042 字节,占用了 (79%) 程序存储空间。最大为 1310720 字节。
全局变量使用了35544字节,(10%)的动态内存,余留292136字节局部变量。最大为327680字节。
2、硬件效果ESP-ROM:esp32s2-rc4-20191025
Build:Oct 25 2019
rst:0x1 (POWERON),boot:0xb (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:1
load:0x3ffe6100,len:0x510
load:0x4004c000,len:0xa50
load:0x40050000,len:0x28bc
entry 0x4004c18c
UART Initialized
WiFi connected!
IP:192.168.xxx.xxx
[HTTP] begin...
[HTTP] GET...
[HTTP] GET... code: 200{"ip":"112.9.xxx.xxx","country":"China","cc":"CN"}
[HTTP] begin...
[HTTP] GET...
[HTTP] GET... code: 200{
"status": 0,
"message": "query ok",
"result": {
"ip": "112.9.xxx.xxx",
"location": {
"lat": xxx,
"lng": xxx
},
"ad_info": {
"nation": "中国",
"province": "山东省",
"city": "德州市",
"district": "xxx",
"adcode": xxx
}
}
}
[HTTP] begin...
[HTTP] GET...
[HTTP] GET... code: 200{"results":[{"location":{"id":"WWDTN3X443VR","name":"德州","country":"CN","path":"德州,德州,山东,中国","timezone":"Asia/Shanghai","timezone_offset":"+08:00"},"now":{"text":"多云","code":"4","temperature":"2"},"last_update":"2022-01-27T17:39:42+08:00"}]}
#include <Arduino.h>
#include <HTTPClient.h>
#include <WiFI.h>
#include <SPI.h>
#include <WiFiMulti.h>
#include <U8g2lib.h>
#include <ArduinoJson.h>
#include <time.h>
根据 esp32_audio_v2.2_sch.pdf 中的芯片引脚图的标记确定SPI通信的引脚以及四个按键的引脚。
int PIN_SCK = 36;//IO36
int PIN_SDA = 35;//IO35
int PIN_RST = 34;//IO34
int PIN_DC = 33;//IO33
int PIN_CS = 37;//IO37
int PIN_KEY1 = 1;//IO1
int PIN_KEY2 = 2;//IO2
int PIN_KEY3 = 3;//IO3
int PIN_KEY4 = 6;//IO6
const char* ssid = "xxxx";////网络名称
const char* password = "xxxx";//"2019210194";//网络密码
const char *ntpServer = "cn.ntp.org.cn";//ntp服务器地址
const long gmtOffset_sec = 8*3600;
const int daylightOffset_sec =8*3600;//用于将时间校正到东八区
StaticJsonDocument<500> doc; //存储IP定位的JSON数据
StaticJsonDocument<500> weatherDoc; //存储天气的JSON数据
StaticJsonDocument<200> IPDoc; //存储公网IP的JSON数据
struct tm timeinfo; //存储当前时间
WiFiMulti wifiMulti; //用于连接WiFi
根据屏幕的相关信息选择合适的初始化函数。
屏幕驱动为SSD2306,分辨率为128X64,保存一页的缓冲区,4线 (clock, data, cs 和dc) 软件模拟SPI,因此选择以下初始化函数。
U8G2_SSD1306_128X64_NONAME_1_4W_SW_SPI u8g2(U8G2_R0,PIN_SCK,PIN_SDA,PIN_CS,PIN_DC,PIN_RST);
其中U8G2_R0表示显示画面不旋转。
int GetTime()
{
if (!getLocalTime(&timeinfo))
{
Serial.println("Failed to obtain time");
return 0;
}
// Serial.println(&timeinfo, "%A, %Y-%m-%d %H:%M:%S");
return 1;
}
void getPublicIP()
{
HTTPClient http; //初始化HTTP对象
Serial.println("[HTTP] begin...");
String url = "https://api.myip.com/"; //获取公网IP的API
http.begin(url); //HTTP
Serial.println("[HTTP] GET...");
int httpCode = http.GET();
if(httpCode > 0)
{
Serial.print("[HTTP] GET... code: ");
Serial.print(httpCode);
if(httpCode == HTTP_CODE_OK) //若访问成功,将数据装载到相应的StaticJsonDocument中
{
String payload = http.getString();
deserializeJson(IPDoc, payload);
Serial.println(payload); //若访问失败,打印状态码
}else
{
Serial.print("[HTTP] GET... failed, error: ");
Serial.print(http.errorToString(httpCode).c_str());
}
http.end();
}
}
void GetIPPos()
{
HTTPClient http;
Serial.println("[HTTP] begin...");
String url;
String str1 = "https://apis.map.qq.com/ws/location/v1/ip?ip=";
String strIP = IPDoc["ip"];
String str2 = "&key=xxxx";//IP定位API,为保护隐私,已抹去密钥
url=str1 + strIP + str2;
http.begin(url);
Serial.println("[HTTP] GET...");
int httpCode = http.GET();
if(httpCode > 0)
{
Serial.print("[HTTP] GET... code: ");
Serial.print(httpCode);
if(httpCode == HTTP_CODE_OK)
{
String payload = http.getString();
deserializeJson(doc, payload);
Serial.println(payload);
}else
{
Serial.print("[HTTP] GET... failed, error: ");
Serial.print(http.errorToString(httpCode).c_str());
}
http.end();
}
}
利用刚刚获取的位置信息,请求当地天气。
void GetWeather()
{
HTTPClient http;
Serial.println("[HTTP] begin...");
String url;
String str1 = "https://api.seniverse.com/v3/weather/now.json?key=xxxx&location=";
String str2 = "&language=zh-Hans&unit=c";
String strCity = doc["result"]["ad_info"]["city"]; //天气查询API,为保护隐私,已抹去密钥
url=str1 + strCity + str2;
http.begin(url); //HTTP
Serial.println("[HTTP] GET...");
int httpCode = http.GET();
if(httpCode > 0)
{
Serial.print("[HTTP] GET... code: ");
Serial.print(httpCode);
if(httpCode == HTTP_CODE_OK)
{
String payload = http.getString();
deserializeJson(weatherDoc, payload);
Serial.println(payload);
}else
{
Serial.print("[HTTP] GET... failed, error: ");
Serial.print(http.errorToString(httpCode).c_str());
}
http.end();
}
}
void setup() {
// put your setup code here, to run once:
u8g2.begin();//初始化oled驱动
u8g2.enableUTF8Print();
Serial.begin(115200);//初始化串口,波特率为115200
pinMode(PIN_KEY1,INPUT);
pinMode(PIN_KEY2,INPUT);
pinMode(PIN_KEY3,INPUT);//配置三个按钮为输入
Serial.println("UART Initialized");
wifiMulti.addAP(ssid, password);
while(wifiMulti.run()!=WL_CONNECTED) //尝试连接WiFi
{
delay(500);
Serial.println(".");
}
Serial.println("WiFi connected!");
Serial.print("IP:");
Serial.println(WiFi.localIP());
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); //连接ntp服务器,同步时间
GetTime();
if(wifiMulti.run()==WL_CONNECTED) //初次执行完整流程
{
getPublicIP();
GetIPPos();
GetWeather();
}
}
void loop() {
// put your main code here, to run repeatedly:
String PublicIP = IPDoc["ip"];
u8g2.firstPage();
do//显示数据
{
GetTime();
String resPos = doc["result"]["ad_info"]["city"];
String resWeather = weatherDoc["results"][0]["now"]["text"];
String resTemp = weatherDoc["results"][0]["now"]["temperature"];
u8g2.setFont(u8g2_font_wqy14_t_gb2312); //设置字库,字号为14
// u8g2.drawStr(0,15,"Hello World");
u8g2.setCursor(0,15);u8g2.print(&timeinfo,"%Y/%m/%d %H:%M:%S");//规定显示格式
u8g2.setCursor(0,40);u8g2.print(resPos);//位置
u8g2.setCursor(0,60);u8g2.print("天气:");
u8g2.setCursor(40,60);u8g2.print(resWeather);//天气
u8g2.setCursor(80,60);u8g2.print(resTemp);//温度
u8g2.setCursor(95,60);u8g2.print("℃");
}while(u8g2.nextPage());
int buttonState = digitalRead(PIN_KEY1);
int buttonState2 = digitalRead(PIN_KEY2);
int buttonState3 = digitalRead(PIN_KEY3);//读取三个按钮的状态
if(buttonState == LOW)//按键1:更新天气
{
GetWeather();
Serial.println("updating weather...");
delay(50);
}
if(buttonState2 == LOW)//按键2:打印IP信息
{
Serial.print("IP:");
Serial.println(WiFi.localIP());
Serial.print("PublicIP:");
Serial.println(PublicIP);
delay(50);
}
if(buttonState3 == LOW)//按键3:更新时间
{
Serial.println("Getting Time...");
GetTime();
delay(50);
}
}
SPI通信时,CS引脚已经默认接地,但U8g2库的初始化函数需要此参数。因此,在调用初始化函数时,引入一个冗余引脚来代替CS。
2、局域网IP无法定位使用IP定位服务时,若查询局域网IP,会提示无法查询。因此,我们需要先获取本机的公网IP,之后再去获取公网IP的位置,这样才能准确地获取地理位置信息。
3、“霾”字无法加载之前使用的字库大小较小,字库中不包含“霾”等字,因此选择更大的字库,一是可以正常显示部分较为生僻的汉字,二是提高了程序的扩展性。
七、未来的计划或者建议之后如果有时间会尝试移植LVGL,实现多页面显示,并且加入更多流畅的动画效果。