基于ESP32-S2模块的本地气象台/温度计
基于ESP32-S2模块,显示当前网络日记和时间、当地温湿度、风力等级、以及未来3天的天气预报。
标签
Arduino
ESP32
2022寒假在家练
网络时间
天气信息
JSON数据解析
u8g2显示
大写DDG
更新2022-03-03
中北大学
1453

基于ESP32-S2模块的本地气象计/温度仪

一、ESP32开发环境搭建

1.下载 arduino1.xx 版本 (Arduino下载

2.直接点击下载附件中hardware文件并替换掉自己安装目录下的hardware文件夹

(这样子99%可以成功)

3.这样就成功了

二、项目要求分析

制作一个本地气象台/温度计

  • 利用OLED显示
  • 显示当前本地的时间、温度和气象信息

具体实现

  1. 读取网络时间并显示
  2. 读取当地实时天气信息并显示
  3. 读取当地天气预报信息并显示

三、硬件分析

读取网络主要是通过ESP32S2模块实现的,显示主要是通过OLED显示屏来实现的。所以只需要注意ESP32S2模块和OLED显示屏模块之间的连接个别按键即可。

Fk1eJwMBcYTO8t3IsmCFagpZtPyM

 

对于oled显示屏显示可以使用u8g2来适配(需要以下硬件相关定义)

/****************************OLED显示定义********************************/
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
// 使用硬件SPI,只需要定义CS(该板载显示屏幕内部置低,引脚未引出) DC(33) RST(34)即可
U8G2_SSD1306_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 26, /* dc=*/ 33, /* reset=*/ 34);
/**********************************************************************/

 按键需要以下定义(只需要使用一个按键)

const int buttonPin = 1;

四、程序流程分析

FriEe8Y2260Gxvi62yugs9618R6a

五、关键内容具体实现

1.使用u8g2库实现在oled屏幕上的显示(参考arduino中 u8g2 详解

/*
 * 当前天气显示 液晶屏幕显示布局
 */

#include <Arduino.h>
#include <U8g2lib.h>
#include "bmp.h"
/****************************OLED显示定义********************************/
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
// 使用硬件SPI,只需要定义CS(该板载显示屏幕内部置低,引脚未引出) DC(33) RST(34)即可
U8G2_SSD1306_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 26, /* dc=*/ 33, /* reset=*/ 34);
/**********************************************************************/

static const unsigned char bmp0[] U8X8_PROGMEM = {       /* 白天晴 (32 X 32 )*/ 
0x00,0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,
0x00,0x80,0x01,0x04,0x30,0x80,0x01,0x06,0x60,0x80,0x01,0x03,0xC0,0x00,0x80,0x01,
0x80,0xE1,0xC7,0x00,0x00,0x79,0x1E,0x00,0x00,0x0C,0x38,0x00,0x00,0x06,0x20,0x00,
0x00,0x02,0x60,0x00,0x00,0x03,0x40,0x00,0x00,0x03,0xC0,0x00,0x7E,0x01,0xC0,0x7E,
0x7E,0x01,0xC0,0x7E,0x00,0x03,0xC0,0x00,0x00,0x03,0x40,0x00,0x00,0x06,0x60,0x00,
0x00,0x06,0x30,0x00,0x00,0x1C,0x18,0x00,0x00,0xF1,0xCF,0x00,0x80,0xC1,0x83,0x01,
0xC0,0x00,0x00,0x03,0x60,0x80,0x01,0x06,0x30,0x80,0x01,0x04,0x00,0x80,0x01,0x00,
0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x00,0x00,0x00
};

void setup(void) {
  u8g2.begin();
}

void loop(void) {
  u8g2.clearBuffer();
  
  u8g2.drawXBMP( 12,12, 32, 32, bmp0);
  u8g2.setFont(u8g2_font_helvR18_tr);
  u8g2.drawStr( 54,24,"26  C");
  u8g2.drawCircle(88,9,3,U8G2_DRAW_ALL);
  u8g2.setFont(u8g2_font_helvR08_tr);
  u8g2.drawStr( 54,40,"Humi:12%");
  u8g2.drawStr( 54,56,"Wind:5");
  u8g2.sendBuffer();
  delay(3000);
}

之后的显示都建立在这个代码的基础上。

2.获取网络时间并显示(可以参考ESP32获取网络时间

注:以下代码有一个不易发现的BUG,在周一到周六正常,在周日硬件会连续重启(本人实际遇到 说多了都是泪)

#include <U8g2lib.h>
#include <Arduino.h>
#include <WiFi.h>

/****************************OLED显示定义********************************/
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
// 使用硬件SPI,只需要定义CS(该板载显示屏幕内部置低,引脚未引出) DC(33) RST(34)即可
U8G2_SSD1306_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 26, /* dc=*/ 33, /* reset=*/ 34);
/**********************************************************************/


/******************************WiFi连接*********************************/
/*
 * WiFi初始化和连接
 */
void WiFi_Connect()
{
  Serial.print("Connecting...");
  WiFi.begin("TP-LINK_2013", "ylp812787442");
  while (WiFi.status() != WL_CONNECTED){  //这里是阻塞程序,直到连接成功
    delay(300);
    Serial.print(".");
  }
  Serial.println("WiFi connect successed");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println();
}
/*********************************************************************/ 


/***************************3.获取并且显示当前网络时间***************************/
const String WDAY_NAMES[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};                //星期

const char *ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 8 * 3600;
const int daylightOffset_sec = 0;

// 获取成功后芯片会使用RTC时钟保持时间的更新
void printLocalTime()
{
    struct tm timeinfo;
    if (!getLocalTime(&timeinfo))
    {
        Serial.println("Failed to obtain time");
        return;
    }
    //Serial.println(&timeinfo, "%F %T %A"); // 格式化输出
    Serial.print(asctime(&timeinfo)); //默认打印格式:Mon Oct 25 11:13:29 2021
    
    String date = WDAY_NAMES[timeinfo.tm_wday - 1];
    Serial.println(date.c_str()); // 星期的字符输出
    //String year = String(timeinfo.tm_year + 1900); //年
    //String month = String(timeinfo.tm_mon + 1); //月
    //String date = String(timeinfo.tm_mday); //日
    //String hour = String(timeInfo.tm_hour); //时
    //String min = String(timeInfo.tm_min);   //分
    //String sec = String(timeInfo.tm_sec);   //秒
    
    u8g2.setCursor(15, 40);
    u8g2.print(&timeinfo, "%T");  //显示  时:分:秒

    u8g2.setFont(u8g2_font_ncenB08_tr);
    u8g2.setCursor(0, 10);
    u8g2.print(&timeinfo, "%F");  //显示  年:月:日

//    u8g2.setCursor(80, 10);
//    u8g2.print(&timeinfo, "%A");  //显示  星期
    u8g2.drawStr(90,10,date.c_str());

    
}
/************************************************************************/


void setup(void) {
  Serial.begin(115200); // open the serial port at 115200 bps;
  delay(100);
  
  WiFi_Connect();
  
  u8g2.begin();
  u8g2.enableUTF8Print();
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}

void loop(void) {
  u8g2.setFontDirection(0);
  u8g2.clearBuffer();         // 清除内部缓冲区
  u8g2.setFont(u8g2_font_ncenB18_tr); // choose a suitable font
//  u8g2.drawStr(0,20,"21:12:56");  // write something to the internal memory
  printLocalTime();

  u8g2.sendBuffer();          // transfer internal memory to the display
  delay(1000);  
}

获取网络时间不能每时每刻都向时间服务器发出请求,可以将读取到的网络时间同步到自身外设RTC,读取RTC实时时间。

3.获取实时的天气信息(参考arduino环境心知天气使用教程

注:密钥需要填写自己的账号私钥(心知天气官网可以自己注册并获取)

Fj5xD9QEysRHwmLo-aHsLWut_Ip6

/*********************************2.获取并显示当前天气信息****************************/

const char* host = "api.seniverse.com";     // 将要连接的服务器地址  
const int httpPort = 80;                    // 将要连接的服务器端口      
 
// 心知天气HTTP请求所需信息
String reqUserKey = "xxxxxxxxxxxxxxxxx";   // 私钥
String reqLocation = "Taiyuan";            // 城市
String reqUnit = "c";                      // 摄氏/华氏

//  获取当前天气状况
void getNowWeather()
{
  WiFiClient client;
   // 建立心知天气API当前天气请求资源地址
  String reqRes = "/v3/weather/now.json?key=" + reqUserKey +
                  + "&location=" + reqLocation + 
                  "&language=en&unit=" +reqUnit;
  
  // 建立http请求信息
  String httpRequest = String("GET ") + reqRes + " HTTP/1.1\r\n" + 
                              "Host: " + host + "\r\n" + 
                              "Connection: close\r\n\r\n";
  // 尝试连接服务器
  if (!client.connect(host, httpPort)) 
  {
    Serial.println("weather host connection failed");
    return;
  }
  Serial.println("weather host connection successed");
  
  // 向服务器发送http请求信息
  client.print(httpRequest);
  //Serial.println("Sending request: ");
  //Serial.println(httpRequest);

  // 获取并显示服务器响应状态行 200/404
  String status_response = client.readStringUntil('\n');
  //Serial.print("status_response: ");
  //Serial.println(status_response);

  // 使用find跳过HTTP响应头
  if (client.find("\r\n\r\n")) 
  {
    //Serial.println("Found Header End. Start Parsing.");
  }

  // 以下为解析JSON天气数据
  const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(6) + 230;
  DynamicJsonDocument doc(capacity);
  
  deserializeJson(doc, client);
  
  JsonObject results_0 = doc["results"][0];
  
  JsonObject results_0_now = results_0["now"];
  const char* results_0_now_text = results_0_now["text"]; // "Sunny"
  const char* results_0_now_code = results_0_now["code"]; // "0"
  const char* results_0_now_temperature = results_0_now["temperature"]; // "32"
  
  const char* results_0_last_update = results_0["last_update"]; // "2020-06-02T14:40:00+08:00" 
 
  // 通过串口监视器显示以上信息
  String results_0_now_text_str = results_0_now["text"].as<String>(); 
  int results_0_now_code_int = results_0_now["code"].as<int>(); 
  int results_0_now_temperature_int = results_0_now["temperature"].as<int>(); 
  
  String results_0_last_update_str = results_0["last_update"].as<String>();   
 
  Serial.println(F("======Weahter Now======="));
  Serial.print(F("Weather Now: "));
  Serial.print(results_0_now_text_str);
  Serial.print(F(" "));
  Serial.println(results_0_now_code_int);
  Serial.print(F("Temperature: "));
  Serial.println(results_0_now_temperature_int);
  Serial.println(F("========================"));
  int temp = results_0_now_temperature_int;
  switch(results_0_now_code_int){
    case 0:
    case 38: u8g2.drawXBMP( 12,12, 32, 32, bmp0); break; // 白天晴
    case 1: u8g2.drawXBMP( 12,12, 32, 32, bmp1); break; // 夜晚晴
    case 4:
    case 7:
    case 8: u8g2.drawXBMP( 12,12, 32, 32, bmp2); break; // 多云
    case 5: u8g2.drawXBMP( 12,12, 32, 32, bmp3); break; // 白天晴间多云
    case 6: u8g2.drawXBMP( 12,12, 32, 32, bmp4); break; // 晚上晴间多云
    case 9: u8g2.drawXBMP( 12,12, 32, 32, bmp5); break; // 阴天
    case 10:
    case 11:
    case 12:
    case 13:
    case 14:
    case 15:
    case 16:
    case 17:
    case 18:
    case 19:
    case 20: u8g2.drawXBMP( 12,12, 32, 32, bmp6); break; // 下雨
    case 21:
    case 22:
    case 23:
    case 24:
    case 25:
    case 37: u8g2.drawXBMP( 12,12, 32, 32, bmp7); break; // 下雪
    case 26:
    case 27:
    case 28:
    case 29:
    case 32:
    case 33:
    case 34:
    case 35:
    case 36: u8g2.drawXBMP( 12,12, 32, 32, bmp8); break; // 风
    case 30:
    case 31: u8g2.drawXBMP( 12, 12, 32, 32, bmp9); break; // 雾霾
    default: ;
  }
  u8g2.setFont(u8g2_font_helvR18_tr);
//  u8g2.drawStr( 54,24,temp);
  u8g2.setCursor(54, 24);
  u8g2.printf("%d  C", temp);  //显示  时:分:秒
  if(temp>=0){
    if(temp/10!=0)
      u8g2.drawCircle(88,9,3,U8G2_DRAW_ALL);
    else
      u8g2.drawCircle(72,9,3,U8G2_DRAW_ALL);
  }
  else{
    if(temp/10!=0)
      u8g2.drawCircle(98,9,3,U8G2_DRAW_ALL);
    else
      u8g2.drawCircle(82,9,3,U8G2_DRAW_ALL);
  }
  
  
  //断开客户端与服务器连接工作
  client.stop(); 
  Serial.println("closing connection");
  Serial.println();
}
/*********************************************************************************/

4.获取天气预报信息并显示

/*********************************3.获取并显示三天天气预报信息****************************/
int getForecastWeather(){
  int a;
  WiFiClient client;
  
  // 建立心知天气API当前天气请求资源地址
  String reqRes = "/v3/weather/daily.json?key=" + reqUserKey +
                  + "&location=" + reqLocation + "&language=en&unit=" +
                  reqUnit + "&start=0&days=3";
 
  // 建立http请求信息
  String httpRequest = String("GET ") + reqRes + " HTTP/1.1\r\n" + 
                              "Host: " + host + "\r\n" + 
                              "Connection: close\r\n\r\n";
  Serial.println(""); 
  Serial.print("Connecting to "); Serial.print(host);
 
  // 尝试连接服务器
  if (client.connect(host, 80)){
    Serial.println(" Success!");
 
    // 向服务器发送http请求信息
    client.print(httpRequest);
    Serial.println("Sending request: ");
    Serial.println(httpRequest);  
 
    // 获取并显示服务器响应状态行 
    String status_response = client.readStringUntil('\n');
    Serial.print("status_response: ");
    Serial.println(status_response);
 
  // 使用find跳过HTTP响应头
  if (client.find("\r\n\r\n")) {
    Serial.println("Found Header End. Start Parsing.");
  }
    
  // 利用ArduinoJson库解析心知天气响应信息 

  const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_ARRAY_SIZE(3) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(6) + 3*JSON_OBJECT_SIZE(14) + 860;

  DynamicJsonDocument doc(capacity);
  
  deserializeJson(doc, client);
  
  JsonObject results_0 = doc["results"][0];
  
  JsonArray results_0_daily = results_0["daily"];
  
  JsonObject results_0_daily_0 = results_0_daily[0];
  const char* results_0_daily_0_date = results_0_daily_0["date"]; 
  const char* results_0_daily_0_text_day = results_0_daily_0["text_day"]; 
  const char* results_0_daily_0_code_day = results_0_daily_0["code_day"];
  const char* results_0_daily_0_text_night = results_0_daily_0["text_night"]; 
  const char* results_0_daily_0_code_night = results_0_daily_0["code_night"]; 
  const char* results_0_daily_0_high = results_0_daily_0["high"];
  const char* results_0_daily_0_low = results_0_daily_0["low"]; 
  const char* results_0_daily_0_rainfall = results_0_daily_0["rainfall"];
  const char* results_0_daily_0_precip = results_0_daily_0["precip"]; 
  const char* results_0_daily_0_wind_direction = results_0_daily_0["wind_direction"]; 
  const char* results_0_daily_0_wind_direction_degree = results_0_daily_0["wind_direction_degree"];
  const char* results_0_daily_0_wind_speed = results_0_daily_0["wind_speed"];
  const char* results_0_daily_0_wind_scale = results_0_daily_0["wind_scale"];
  const char* results_0_daily_0_humidity = results_0_daily_0["humidity"];
  
  JsonObject results_0_daily_1 = results_0_daily[1];
  const char* results_0_daily_1_date = results_0_daily_1["date"];
  const char* results_0_daily_1_text_day = results_0_daily_1["text_day"];
  const char* results_0_daily_1_code_day = results_0_daily_1["code_day"];
  const char* results_0_daily_1_text_night = results_0_daily_1["text_night"]; 
  const char* results_0_daily_1_code_night = results_0_daily_1["code_night"]; 
  const char* results_0_daily_1_high = results_0_daily_1["high"];
  const char* results_0_daily_1_low = results_0_daily_1["low"]; 
  const char* results_0_daily_1_rainfall = results_0_daily_1["rainfall"]; 
  const char* results_0_daily_1_precip = results_0_daily_1["precip"]; 
  const char* results_0_daily_1_wind_direction = results_0_daily_1["wind_direction"];
  const char* results_0_daily_1_wind_direction_degree = results_0_daily_1["wind_direction_degree"]; 
  const char* results_0_daily_1_wind_speed = results_0_daily_1["wind_speed"];
  const char* results_0_daily_1_wind_scale = results_0_daily_1["wind_scale"];
  const char* results_0_daily_1_humidity = results_0_daily_1["humidity"]; 
  
  JsonObject results_0_daily_2 = results_0_daily[2];
  const char* results_0_daily_2_date = results_0_daily_2["date"];
  const char* results_0_daily_2_text_day = results_0_daily_2["text_day"];
  const char* results_0_daily_2_code_day = results_0_daily_2["code_day"];
  const char* results_0_daily_2_text_night = results_0_daily_2["text_night"];
  const char* results_0_daily_2_code_night = results_0_daily_2["code_night"];
  const char* results_0_daily_2_high = results_0_daily_2["high"]; 
  const char* results_0_daily_2_low = results_0_daily_2["low"]; 
  const char* results_0_daily_2_rainfall = results_0_daily_2["rainfall"];
  const char* results_0_daily_2_precip = results_0_daily_2["precip"]; 
  const char* results_0_daily_2_wind_direction = results_0_daily_2["wind_direction"]; 
  const char* results_0_daily_2_wind_direction_degree = results_0_daily_2["wind_direction_degree"]; 
  const char* results_0_daily_2_wind_speed = results_0_daily_2["wind_speed"];
  const char* results_0_daily_2_wind_scale = results_0_daily_2["wind_scale"]; 
  const char* results_0_daily_2_humidity = results_0_daily_2["humidity"]; 
  
  const char* results_0_last_update = results_0["last_update"]; 
  
  // 从以上信息中摘选几个通过串口监视器显示
  day0_date = results_0_daily_0["date"].as<String>();
  day0_code = results_0_daily_0["code_day"].as<int>();
  day0_high = results_0_daily_0["high"].as<int>();
  day0_low  = results_0_daily_0["low"].as<int>();
  humi = results_0_daily_0["humidity"].as<int>();
  wind = results_0_daily_0["wind_scale"].as<int>();

  day1_date = results_0_daily_1["date"].as<String>();
  day1_code = results_0_daily_1["code_day"].as<int>();
  day1_high = results_0_daily_1["high"].as<int>();
  day1_low  = results_0_daily_1["low"].as<int>();

  day2_date = results_0_daily_2["date"].as<String>();
  day2_code = results_0_daily_2["code_day"].as<int>();
  day2_high = results_0_daily_2["high"].as<int>();
  day2_low  = results_0_daily_2["low"].as<int>();
  day0_date =day0_date.substring(5,10); //截取指定位置字符串
  day1_date =day1_date.substring(5,10);
  day2_date =day2_date.substring(5,10);
 
  Serial.println(F("======Today Weahter ======="));
  Serial.print(F("DATE: "));
  Serial.println(day0_date);
  Serial.print(F("Weather code: "));
  Serial.println(day0_code);
  Serial.print(F("Highest tempeture:"));
  Serial.println(day0_high);
  Serial.print(F("Lowest tempeture:"));
  Serial.println(day0_low);

  Serial.print(F("DATE: "));
  Serial.println(day1_date);
  Serial.print(F("Weather code: "));
  Serial.println(day1_code);
  Serial.print(F("Highest tempeture:"));
  Serial.println(day1_high);
  Serial.print(F("Lowest tempeture:"));
  Serial.println(day1_low);

  Serial.print(F("DATE: "));
  Serial.println(day2_date);
  Serial.print(F("Weather code: "));
  Serial.println(day2_code);
  Serial.print(F("Highest tempeture:"));
  Serial.println(day2_high);
  Serial.print(F("Lowest tempeture:"));
  Serial.println(day2_low);
  Serial.println(F("=============================="));
  a = 1;
  }
  else {
    a = 0;
    Serial.println(" connection failed!");
  }   
  //断开客户端与服务器连接工作
  client.stop(); 
  return a;// 1:获取成功  0:获取失败
}

以上两个代码块主要涉及有

  1. 建立HTTP请求信息
  2. 向服务器发送请求
  3. 获取服务器响应状态和响应信息
  4. 解析响应信息(arduinoison在线解析网站

以上1,2,3 三点都是固定方法,关键是数据获取和处理。

五、程序整合及实现关键点

  • 将天气信息的获取和显示分开为两个函数。
  • 按键只负责切换显示,不负责刷新数据。
  • loop使用delay会影响按键扫描,所以1s和1min定时使用非阻塞方式。

六、可改进地方

  1. 屏幕布局不够好看,字体选择观感欠佳。
  2. 多个页面可以添加倒计时自动切换效果
  3. 连接wifi时候可以添加动态显示效果
  4. 由于心知天气网站服务器限制,实时天气无法读取更多内容

七、心得体会

  1. arduino不是单纯的C编程,而是混合了部分C++,很多库无法直观的观看,很多函数也是arduino所特有的。
  2. arduino编译速度及其缓慢,严重影响使用体验。(前期使用vs_code+platformio感觉不错,但是没有S2的arduino开发框架
  3. 本人尝试过micropython开发,资料较少,且ESP32的部分外设功能适配性不好。封装程度更高。
  4. 本人搭建esp-idf开发环境n多次才成功,但是后期由于网上相关教程较少,才放弃。我觉得乐鑫或者安信可可以开发在不同系统的适配效果好的ide。

 

软硬件
元器件
ESP32-S2-MINI-1
2.4GHz Wi­Fi (802.11 b/g/n) 模组, 内置ESP32­S2系列芯片,Xtensa® 单核32位LX7微处理器, 内置芯片叠封4MB flash,可叠封2MB PSRAM, 37个GPIO,丰富的外设, 板载PCB天线或外部天线连接器
附件下载
06_final_version_v1.ino.esp32s2.bin
可以直接烧写的二进制文件(WiFi账号和密码是个人的,不改的话无法使用)
06_final_version_v1.zip
源工程文件(若要验证功能,请修改36行的WiFi账号和密码)
hardware.zip
esp32的硬件支持包(在arduino安装目录中直接粘贴替换)
团队介绍
中北大学信息与通信工程学院
团队成员
董殿国
中北大学信息与通信工程学院研究生二年级在读
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号