ESP32智能天气时钟:温湿度气压全掌控

1、环境

①环境硬件:

主板:ESP32-S3 开发板 ESP32 SuperMini

TFT屏幕:2.0寸TFT模块串口屏240x320液晶显示屏ST7789V

温湿度传感器:AHT20+BMP280温湿度气压模块

②软件环境:

ArduinoIDE2.3.7 + esp32 2.0.14

2、功能

①连接Wifi显示星期、日期、时间(秒数每秒变化一次)

②连接Wifi请求和风天气API显示天气(晴,温度,风向,风级),根据天气显示图标

③连接温湿度传感器,显示室内温度,湿度,气压

3、主要解决的问题

①七段数字格式显示

②字体的设置,字体文件的制作

③图片的显示,图片 .h文件的制作

④和风天气API返回GZIP数据的处理

⑤开发板的安装

⑥各种库的安装

cpp 复制代码
// ===================== 核心优化:内置轻量级Gzip解压核心代码【无需任何库】✅ 最关键瘦身点 =====================
#include <zlib.h>
#define GZIP_WINDOW_BITS 16 + MAX_WBITS
#define DECOMPRESS_BUFFER_SIZE 512
#define COMPRESS_BUFFER_SIZE 512

#define COLOR_1B2B3A 0x0000//黑色

uint32_t gzipDecompress(uint8_t *inData, uint32_t inLen, uint8_t *outData, uint32_t outLen) {
  z_stream strm;
  memset(&strm, 0, sizeof(z_stream));
  strm.next_in = inData;
  strm.avail_in = inLen;
  strm.next_out = outData;
  strm.avail_out = outLen;

  if (inflateInit2(&strm, GZIP_WINDOW_BITS) != Z_OK) return 0;
  int ret = inflate(&strm, Z_FINISH);
  inflateEnd(&strm);
  return (ret == Z_STREAM_END) ? strm.total_out : 0;
}

// 获取网络时间相关库
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>

// TFT显示库
#include <TFT_eSPI.h>

// 网络请求+JSON解析库
#include <HTTPClient.h>
#include <ArduinoJson.h>

// 仅保留核心I2C库 零依赖
#include <Wire.h>

#define I2C_SDA 7        // SDA引脚,固定接7
#define I2C_SCL 6        // SCL引脚,固定接6
#define READ_DELAY 1000  // 数据读取间隔

// AHT20 寄存器定义
#define AHT20_ADDR 0x38
// BMP280 寄存器定义
#define BMP280_ADDR 0x76
#define BMP280_RESET 0xE0
#define BMP280_CTRL_MEAS 0xF4
#define BMP280_CONFIG 0xF5
#define BMP280_PRESS_MSB 0xF7
#define BMP280_TEMP_MSB 0xFA

// 导入字库
#include "font/noto20.h"
#include "font/SeverSegmentNumber28.h"
#include "font/SeverSegmentNumber68.h"
#include "font/WeatherFont30.h"
#include "font/tianqi28.h"
#include "font/WeatherFont25.h"
#include "WeatherIcon/sun.h"
#include "WeatherIcon/cloud.h"
#include "WeatherIcon/duoyun.h"
#include "WeatherIcon/yin.h"
#include "WeatherIcon/rain.h"


// ===================== 配置项 无修改 =====================
const char *ssid = "ah-student";
const char *password = "AIhuaWx2024";

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp.aliyun.com");

const char *WEATHER_API_KEY = "c049f972cfb44a55afecdcd77fc64580";
const char *CITY_ID = "101120501";
const int WEATHER_REFRESH_INTERVAL = 10 * 60 * 1000;
const int NTP_UPDATE_INTERVAL = 60 * 1000;

// TFT相关定义
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite sprite = TFT_eSprite(&tft);

// ===================== 全局变量 精简冗余 + 初始时间默认值 =====================
const char weekdays_cn[8][10] = { "0", "周一", "周二", "周三", "周四", "周五", "周六", "周日" };
// ✅ 初始默认时间:防止NTP首次获取失败时显示0
int currentYear = 2026;
int currentWeekDay = 1;
int currentMonth = 1;
int currentMonthDay = 1;
int currentHour = 0;
int currentMin = 0;
int currentSec = 0;

String weatherText = "未知";
int temp = 25;
int humidity = 50;
unsigned long lastWeatherUpdate = 0;
unsigned long lastNtpUpdate = 0;
int weatherIconCode = 100;

bool showClockStyle = true;
unsigned long lastUpdateTime = 0;
unsigned long lastStyleSwitch = 0;
const int STYLE_SWITCH_INTERVAL = 10 * 1000;

float pressure_hpa = 1013.25;
float altitude_m = 0.0;
#define SEA_LEVEL_PRESSURE 1013.25

uint16_t dig_T1, dig_P1;
int16_t dig_T2, dig_T3, dig_P2, dig_P3, dig_P4, dig_P5, dig_P6, dig_P7, dig_P8, dig_P9;

// ========== ✅ 数字补零格式化【传参缓冲区 线程安全版】 ==========
void formatTwoDigits(int num, char *buf) {
  buf[0] = (num < 10) ? '0' : (num / 10) + '0';
  buf[1] = (num % 10) + '0';
  buf[2] = '\0';
}

// I2C基础驱动
void I2C_Write(uint8_t addr, uint8_t reg, uint8_t data) {
  Wire.beginTransmission(addr);
  Wire.write(reg);
  Wire.write(data);
  Wire.endTransmission();
}

void I2C_Read(uint8_t addr, uint8_t reg, uint8_t *buf, uint8_t len) {
  Wire.beginTransmission(addr);
  Wire.write(reg);
  Wire.endTransmission(false);
  Wire.requestFrom(addr, len);
  for (uint8_t i = 0; i < len; i++) buf[i] = Wire.read();
}

// AHT20温湿度读取
void readAHT20() {
  uint8_t data[6] = { 0 };
  Wire.beginTransmission(AHT20_ADDR);
  Wire.write(0xAC);
  Wire.write(0x33);
  Wire.write(0x00);
  Wire.endTransmission();
  delayMicroseconds(80000);

  Wire.requestFrom(AHT20_ADDR, 6);
  if (Wire.available() == 6)
    for (uint8_t i = 0; i < 6; i++) data[i] = Wire.read();

  if ((data[0] & 0x80) == 0 && (data[0] & 0x08) == 0x08) {
    long humi = ((long)data[1] << 12) | ((long)data[2] << 4) | (data[3] >> 4);
    long temp_raw = (((long)data[3] & 0x0F) << 16) | ((long)data[4] << 8) | (long)data[5];
    humidity = constrain((int)(humi * 100.0 / 0x100000), 0, 100);
    temp = constrain((int)(temp_raw * 200.0 / 0x100000 - 50), -20, 80);
  }
}

// BMP280初始化
void BMP280_Init() {
  uint8_t buf[24];
  I2C_Write(BMP280_ADDR, BMP280_RESET, 0xB6);
  delay(100);
  I2C_Read(BMP280_ADDR, 0x88, buf, 24);

  dig_T1 = (buf[1] << 8) | buf[0];
  dig_T2 = (buf[3] << 8) | buf[2];
  dig_T3 = (buf[5] << 8) | buf[4];
  dig_P1 = (buf[7] << 8) | buf[6];
  dig_P2 = (buf[9] << 8) | buf[8];
  dig_P3 = (buf[11] << 8) | buf[10];
  dig_P4 = (buf[13] << 8) | buf[12];
  dig_P5 = (buf[15] << 8) | buf[14];
  dig_P6 = (buf[17] << 8) | buf[16];
  dig_P7 = (buf[19] << 8) | buf[18];
  dig_P8 = (buf[21] << 8) | buf[20];
  dig_P9 = (buf[23] << 8) | buf[22];

  I2C_Write(BMP280_ADDR, BMP280_CTRL_MEAS, 0x77);
  I2C_Write(BMP280_ADDR, BMP280_CONFIG, 0x00);
}

// BMP280气压海拔读取
void readBMP280() {
  int32_t temp_raw, press_raw;
  int32_t t_fine;
  uint8_t data[6] = { 0 };

  I2C_Read(BMP280_ADDR, BMP280_TEMP_MSB, data, 6);
  temp_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
  press_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4);

  int64_t var1 = ((((temp_raw >> 3) - ((int32_t)dig_T1 << 1))) * ((int32_t)dig_T2)) >> 11;
  int64_t var2 = (((((temp_raw >> 4) - ((int32_t)dig_T1)) * ((temp_raw >> 4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
  t_fine = var1 + var2;

  int64_t p = t_fine - 128000;
  p = (((press_raw >> 13) - (((int32_t)dig_P1) << 1)) - (((p * p) >> 12) * ((int32_t)dig_P2)) >> 11) * (((((p * p) >> 13) * ((int32_t)dig_P3)) >> 16) + (((int32_t)dig_P4) * p) >> 15);
  p = p + (((int32_t)dig_P5) << 4);
  p = p - (((((p >> 15) * (p >> 15)) >> 3) * ((int32_t)dig_P6)) >> 4);
  p = p + (((int32_t)dig_P7) * p) >> 15;
  p = p + (((int32_t)dig_P8) << 10);
  p = p + dig_P9;
  p = p >> 8;

  if (p > 0) pressure_hpa = (float)p / 256.0;
  if (pressure_hpa > 300 && pressure_hpa < 1200) altitude_m = 44330.0 * (1.0 - pow(pressure_hpa / SEA_LEVEL_PRESSURE, 0.1903));
}

// 读取传感器数据
void readSensorData() {
  readAHT20();
  readBMP280();
}

// WiFi断线自动重连
void checkWiFiReconnect() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.print("WiFi重连中...");
    WiFi.disconnect();
    WiFi.begin(ssid, password);
    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 20) {
      delay(500);
      Serial.print(".");
      retry++;
    }
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("\nWiFi重连成功");
      getYantaiWeather();
    } else Serial.println("\nWiFi重连失败");
  }
}

// ========== Gzip解压和风天气【轻量级内置解压,无库依赖】 ==========
void getYantaiWeather() {
  checkWiFiReconnect();
  if (WiFi.status() != WL_CONNECTED) {
    weatherText = "无网络";
    return;
  }

  HTTPClient http;
  String url = "https://mw6apx8fr5.re.qweatherapi.com/v7/weather/now?location=" 
  + String(CITY_ID) + "&key=" + String(WEATHER_API_KEY);
  http.begin(url);
  http.addHeader("Accept", "application/json");
  http.addHeader("User-Agent", "ESP32");
  http.addHeader("Accept-Encoding", "gzip");
  http.setTimeout(5000);
  int httpCode = http.GET();

  if (httpCode == HTTP_CODE_OK) {
    uint8_t compressedBuf[COMPRESS_BUFFER_SIZE] = { 0 };
    uint8_t decompressedBuf[DECOMPRESS_BUFFER_SIZE] = { 0 };
    size_t compressLen = http.getStreamPtr()->readBytes(compressedBuf, COMPRESS_BUFFER_SIZE);

    if (compressLen > 0) {
      size_t decompressLen = gzipDecompress(compressedBuf, compressLen, decompressedBuf, DECOMPRESS_BUFFER_SIZE);
      if (decompressLen > 0) {
        DynamicJsonDocument doc(256);
        DeserializationError error = deserializeJson(doc, (char *)decompressedBuf);
        if (!error) {
          weatherText = doc["now"]["text"].as<String>() + ","
                        + doc["now"]["temp"].as<String>() + "°" + ","
                        + doc["now"]["windDir"].as<String>() + doc["now"]["windScale"].as<String>()+"级";
          weatherIconCode = doc["now"]["icon"].as<int>();
        } else weatherText = "本地气象";
      } else weatherText = "解压失败";
    } else weatherText = "数据为空";
  } else weatherText = "请求失败";

  http.end();
  lastWeatherUpdate = millis();
}
// 根据天气编码显示对应图标
void showWeatherIcon(int x, int y) {
  switch(weatherIconCode){
    //case 100: for(int i = 0;i<frames;i++){tft.pushImage(x,y,32,32,sun_32x32[i]);delay(100)};break;// 晴→太阳(黄色)
    case 100: sprite.pushImage(x,y,60,60,sun);break;// 晴→太阳(黄色)
    case 101: sprite.pushImage(x,y,60,60,duoyun);break;// 多云→云朵(白色)
    case 102: sprite.pushImage(x,y,60,60,duoyun);break; break;// 少云→云朵
    case 104: sprite.pushImage(x,y,60,60,cloud);break; // 阴→阴云
    case 305: sprite.pushImage(x,y,60,60,rain);break;  // 小雨→雨滴
    case 306: sprite.pushImage(x,y,60,60,rain);break; // 中雨→雨滴
    case 307: sprite.pushImage(x,y,60,60,rain);break;  // 大雨→雨滴
    //case 401: sprite.pushImage(x,y,60,60,snow);break;  // 大雨→雨滴
    default:  sprite.pushImage(x,y,60,60,sun);break; // 默认太阳
  }
}
// ========== ✅ 新增核心函数:将NTP时间同步到全局变量【初始化专用+updateTime内部调用】 ==========
void syncNtpTimeToGlobal() {
  unsigned long epochTime = timeClient.getEpochTime();
  struct tm *ptm = localtime((time_t *)&epochTime);
  currentYear = ptm->tm_year + 1900;
  currentMonth = ptm->tm_mon + 1;
  currentMonthDay = ptm->tm_mday;
  currentWeekDay = (ptm->tm_wday == 0) ? 7 : ptm->tm_wday;
  currentHour = ptm->tm_hour;
  currentMin = ptm->tm_min;
  currentSec = ptm->tm_sec;
  // 串口打印同步的真实时间,方便调试
  Serial.printf("✅ NTP时间同步成功:%04d/%02d/%02d %02d:%02d:%02d %s\n", currentYear, currentMonth, currentMonthDay, currentHour, currentMin, currentSec, weekdays_cn[currentWeekDay]);
}

// ✅ 修复核心逻辑+时区BUG:NTP时间更新 精准走时 无延迟
void updateTime() {
  unsigned long currentMillis = millis();
  if (currentMillis - lastNtpUpdate >= NTP_UPDATE_INTERVAL) {
    lastNtpUpdate = currentMillis;
    if (timeClient.update()) {
      syncNtpTimeToGlobal();  // ✅ 调用同步函数,直接赋值全局变量
      return;
    }
  }
  // 秒数本地自增 精准无延迟 进位逻辑完善
  currentSec++;
  if (currentSec >= 60) {
    currentSec = 0;
    currentMin++;
    if (currentMin >= 60) {
      currentMin = 0;
      currentHour++;
      if (currentHour >= 24) currentHour = 0;
    }
  }
  Serial.printf("%02d:%02d:%02d | %s | %d℃ %d%% | %.1fhPa\n", currentHour, currentMin, currentSec, weatherText.c_str(), temp, humidity, pressure_hpa);
}

// ========== 样式1:标准时钟样式【带冒号 你修改的全部保留+排版美化】 ==========
void clockStyle() {
  sprite.fillScreen(COLOR_1B2B3A);
  char numBuf[3];  // 临时缓冲区,用于补零格式化
  // ✅ 星期+日期 同一行 统一字体 无错位
  sprite.setTextColor(TFT_GREEN);
  sprite.setCursor(25, 30);
  sprite.loadFont(noto20);
  sprite.print(weekdays_cn[currentWeekDay]);
  sprite.print("  ");
  formatTwoDigits(currentMonth, numBuf);
  sprite.print(String(currentYear) + "/" + numBuf + "/");
  formatTwoDigits(currentMonthDay, numBuf);
  sprite.print(numBuf);
  sprite.unloadFont();

  // ✅ 时分 青蓝色 TFT_CYAN 大号字体 居中显示
  sprite.setTextColor(TFT_BLUE);
  sprite.setCursor(30, 80);
  sprite.loadFont(SeverSegmentNumber68);
  formatTwoDigits(currentHour, numBuf);
  sprite.print(numBuf);
  sprite.setCursor(105, 70);
  sprite.print(":");
  sprite.setCursor(120, 80);
  formatTwoDigits(currentMin, numBuf);
  sprite.print(numBuf);
  sprite.unloadFont();

  // ✅ 秒数 绿色 TFT_GREEN 实时跳动
  sprite.setTextColor(TFT_GREEN);
  sprite.setCursor(195, 100);
  sprite.loadFont(SeverSegmentNumber28);
  formatTwoDigits(currentSec, numBuf);
  sprite.print(numBuf);
  sprite.unloadFont();

  // ✅ 烟台天气: + 右侧对齐
  sprite.setTextColor(TFT_BLUE);
  sprite.setCursor(20, 160);
  sprite.loadFont(WeatherFont30);
  sprite.print("烟台天气:");
  sprite.setTextColor(TFT_MAGENTA);
  sprite.setCursor(20, 210);
  sprite.loadFont(WeatherFont25);
  sprite.print(weatherText);
  sprite.unloadFont();
  showWeatherIcon(160, 140);//图标

  sprite.setTextColor(TFT_OLIVE);
  sprite.setCursor(20, 250);
  sprite.loadFont(tianqi28);
  sprite.print("温湿度: ");
  sprite.setCursor(120, 255);
  sprite.loadFont(SeverSegmentNumber28);
  sprite.print(String(temp) + " C " + String(humidity) + "%");
  sprite.unloadFont();

  sprite.setTextColor(TFT_ORANGE);
  sprite.setCursor(20, 285);
  sprite.loadFont(tianqi28);
  sprite.print("气压: ");
  sprite.setCursor(100, 290);
  sprite.loadFont(SeverSegmentNumber28);
  sprite.print(String(pressure_hpa, 1) + "hPa");
  sprite.unloadFont();

  sprite.pushSprite(0, 0);
}

// ========== 样式2:简约时钟样式【无冒号 配色统一】 ==========
void moonClockStyle() {
  sprite.fillScreen(COLOR_1B2B3A);
  char numBuf[3];  // 临时缓冲区,用于补零格式化
  // ✅ 星期+日期 同一行
  sprite.setTextColor(TFT_GREEN);
  sprite.setCursor(25, 30);
  sprite.loadFont(noto20);
  sprite.print(weekdays_cn[currentWeekDay]);
  sprite.print("  ");
  formatTwoDigits(currentMonth, numBuf);
  sprite.print(String(currentYear) + "/" + numBuf + "/");
  formatTwoDigits(currentMonthDay, numBuf);
  sprite.print(numBuf);
  sprite.unloadFont();

  // ✅ 时分 青蓝色 无冒号
  sprite.setTextColor(TFT_BLUE);
  sprite.setCursor(30, 80);
  sprite.loadFont(SeverSegmentNumber68);
  formatTwoDigits(currentHour, numBuf);
  sprite.print(numBuf);
  sprite.setCursor(105, 70);
  sprite.print(":");
  sprite.setCursor(120, 80);
  formatTwoDigits(currentMin, numBuf);
  sprite.print(numBuf);
  sprite.unloadFont();

  // ✅ 秒数 绿色 实时跳动
  sprite.setTextColor(TFT_GREEN);
  sprite.setCursor(195, 100);
  sprite.loadFont(SeverSegmentNumber28);
  formatTwoDigits(currentSec, numBuf);
  sprite.print(numBuf);
  sprite.unloadFont();

  // ✅ 烟台天气: + 右侧对齐
  sprite.setTextColor(TFT_YELLOW);
  sprite.setCursor(20, 160);
  sprite.loadFont(WeatherFont30);
  sprite.print("烟台天气:");
  sprite.setTextColor(TFT_MAGENTA);
  sprite.setCursor(20, 210);
  sprite.loadFont(WeatherFont25);
  sprite.print(weatherText);
  sprite.unloadFont();
  showWeatherIcon(160, 140);//图标

  sprite.setTextColor(TFT_OLIVE);
  sprite.setCursor(20, 250);
  sprite.loadFont(tianqi28);
  sprite.print("温湿度: ");
  sprite.setCursor(120, 255);
  sprite.loadFont(SeverSegmentNumber28);
  sprite.print(String(temp) + " C " + String(humidity) + "%");
  sprite.unloadFont();

  sprite.setTextColor(TFT_ORANGE);
  sprite.setCursor(20, 285);
  sprite.loadFont(tianqi28);
  sprite.print("气压: ");
  sprite.setCursor(100, 290);
  sprite.loadFont(SeverSegmentNumber28);
  sprite.print(String(pressure_hpa, 1) + "hPa");
  sprite.unloadFont();

  sprite.pushSprite(0, 0);
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  // WiFi连接
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi已连接");

  // NTP初始化 + ✅ 强制获取时间(烧录后立即拿到真实时间)
  timeClient.begin();
  timeClient.setTimeOffset(28800);  // 东八区
  Serial.print("正在获取NTP时间...");
  // 循环强制获取,直到成功
  while (!timeClient.forceUpdate()) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nNTP时间获取成功!");
  syncNtpTimeToGlobal();  // ✅ 核心修复:获取成功后立即同步到全局变量

  // TFT初始化
  tft.begin();
  tft.setRotation(0);
  sprite.setColorDepth(16);
  sprite.setSwapBytes(true);
  
  // 屏幕初始化填充该颜色
  tft.fillScreen(COLOR_1B2B3A);
  sprite.createSprite(240, 320);

  // 传感器初始化
  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(400000);
  BMP280_Init();
  readSensorData();  // 读取初始温湿度气压

  // 获取初始天气
  getYantaiWeather();
  // ✅ 烧录后立即绘制屏幕,无需等待loop的1秒延迟
  clockStyle();
  
  Serial.println("初始化完成,屏幕已显示!");
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - lastWeatherUpdate >= WEATHER_REFRESH_INTERVAL) getYantaiWeather();

  // 每秒刷新 秒数实时变化
  if (currentMillis - lastUpdateTime >= 1000) {
    lastUpdateTime = currentMillis;
    readSensorData();
    updateTime();
    showClockStyle ? clockStyle() : moonClockStyle();
  }

  // 10秒切换样式
  if (currentMillis - lastStyleSwitch >= STYLE_SWITCH_INTERVAL) {
    lastStyleSwitch = currentMillis;
    showClockStyle = !showClockStyle;
  }
}
相关推荐
蝎蟹居2 小时前
GBT 4706.1-2024逐句解读系列(26) 第7.6条款:正确使用符号标识
人工智能·单片机·嵌入式硬件·物联网·安全
水果里面有苹果2 小时前
3-ATSAMV71Q21-ASF
嵌入式硬件
自由的好好干活3 小时前
UBI镜像文件打包与编辑
linux·嵌入式硬件
F133168929574 小时前
5G矿山车载监控终端山河矿卡定位监控终端
stm32·单片机·嵌入式硬件·5g·51单片机·硬件工程
小郭团队4 小时前
1_5_五段式SVPWM (传统算法反正切+DPWM1)算法理论与 MATLAB 实现详解
人工智能·嵌入式硬件·算法·dsp开发
vsropy4 小时前
keil5无法注释中文
stm32·单片机
csdn_te_download_0044 小时前
Keil5安装教程 基于C51 安装教程与配置完全指南
stm32·单片机·嵌入式硬件
ベadvance courageouslyミ5 小时前
51单片机相关
单片机·51单片机·定时器·pwm·蜂鸣器·中断·独立按键
送外卖的工程师5 小时前
STM32F103 驱动 BMP280 气压温湿度传感器 + OLED 显示教程
stm32·单片机·嵌入式硬件·mcu·物联网·proteus·rtdbs