/*
* ESP8266 户外天气 + 日期显示(精简优化版)
* 功能说明:
* - 固定城市:北京(使用点阵字模显示“北”“京”)
* - 天气图标:仅当天气为 "Sunny"(晴)时显示“晴”字
* - 其他天气状态:直接显示英文文本(避免中文乱码)
* - 温度单位 ℃ 自动跟随数字,不重叠
* - 每10分钟自动更新一次天气
*/
#include <ESP8266WiFi.h> // Wi-Fi 连接库
#include <Wire.h> // I2C 通信库(用于 OLED)
#include <Adafruit_GFX.h> // Adafruit 图形基础库
#include <Adafruit_SSD1306.h> // SSD1306 OLED 驱动库
#include <ESP8266HTTPClient.h> // HTTP 客户端(用于请求天气 API)
#include <WiFiClientSecure.h> // 支持 HTTPS 的安全客户端
#include <ArduinoJson.h> // JSON 解析库
// ============ 用户配置区 ============
#define WIFI_SSID "vivoX200" // ← 你的 Wi-Fi 名称
#define WIFI_PASS "12345678" // ← 你的 Wi-Fi 密码
#define CITY_NAME "beijing" // 城市拼音(小写)
#define WEATHER_API_KEY "你的秘钥" // ← 你在 Seniverse 申请的 API 密钥
#define SCREEN_WIDTH 128 // OLED 屏幕宽度
#define SCREEN_HEIGHT 64 // OLED 屏幕高度
#define OLED_ADDR 0x3C // OLED 的 I2C 地址(通常为 0x3C 或 0x3D)
// ===================================
// 创建 OLED 显示对象
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// 全局变量:存储从 API 获取的数据
String weatherText = "Loading"; // 天气描述(英文)
String temperature = "--"; // 当前温度(摄氏度)
String dateStr = "----/--/--"; // 最后更新日期(YYYY-MM-DD)
// ========== 16x16 点阵中文字模(PROGMEM 存储在 Flash 中)==========
// “北” 字模
static const unsigned char PROGMEM bei_text[] = {
0x00,0x00,0x06,0x40,0x06,0x40,0x06,0x40,0x06,0x40,0x06,0x46,0x7E,0x58,0x06,0x60,
0x06,0x40,0x06,0x40,0x06,0x40,0x06,0x40,0x3E,0x42,0x66,0x42,0x06,0x7E,0x00,0x00
};
// “京” 字模
static const unsigned char PROGMEM jing_text[] = {
0x00,0x00,0x01,0x00,0x01,0x80,0x7F,0xFE,0x00,0x00,0x1F,0xF8,0x18,0x18,0x18,0x18,
0x18,0x18,0x1F,0xF8,0x08,0x80,0x0C,0xB0,0x18,0x98,0x30,0x86,0x03,0x80,0x02,0x00
};
// “晴” 字模(仅 Sunny 时显示)
static const unsigned char PROGMEM qing_text[] = {
0x00,0x20,0x00,0x20,0x7B,0xFE,0x48,0x20,0x49,0xFC,0x48,0x20,0x4B,0xFE,0x78,0x00,
0x49,0xFC,0x49,0x04,0x49,0xFC,0x49,0x04,0x79,0xFC,0x49,0x04,0x01,0x14,0x01,0x08
};
// “℃” 符号字模
static const unsigned char PROGMEM du_text[] = {
0x60,0x00,0x91,0xF4,0x96,0x0C,0x6C,0x04,0x08,0x04,0x18,0x00,0x18,0x00,0x18,0x00,
0x18,0x00,0x18,0x00,0x18,0x00,0x08,0x00,0x0C,0x04,0x06,0x08,0x01,0xF0,0x00,0x00
};
// ================================================================
void setup() {
// 初始化串口(用于调试)
Serial.begin(115200);
// 初始化 I2C 总线
Wire.begin();
// 初始化 OLED 屏幕
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
// 如果初始化失败,进入死循环(防止程序继续运行出错)
for (;;);
}
// 显示启动信息
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 0);
display.println("正在连接 Wi-Fi...");
display.display();
// 连接 Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
// 首次获取天气数据
fetchWeather();
}
// 从 ISO8601 时间字符串(如 "2025-04-05T14:30:00")中提取日期部分 "2025-04-05"
String extractDate(String isoTime) {
if (isoTime.length() >= 10) {
return isoTime.substring(0, 10); // 截取前10个字符
}
return "----/--/--"; // 格式错误时返回占位符
}
// 从心知天气 API 获取当前天气数据
void fetchWeather() {
// 检查 Wi-Fi 是否已连接
if (WiFi.status() != WL_CONNECTED) {
weatherText = "无 Wi-Fi";
temperature = "--";
dateStr = "----/--/--";
return;
}
// 构造 API 请求 URL(使用英文返回,便于匹配 "Sunny")
String url = "https://api.seniverse.com/v3/weather/now.json?key=" + String(WEATHER_API_KEY)
+ "&location=" + CITY_NAME + "&language=en&unit=c";
// 创建安全的 HTTPS 客户端(跳过证书验证,仅用于开发)
WiFiClientSecure client;
client.setInsecure(); // ⚠️ 注意:生产环境应验证证书
// 发起 HTTP GET 请求
HTTPClient http;
http.begin(client, url);
int httpResponseCode = http.GET();
Serial.println(url);
if (httpResponseCode == 200) {
// 请求成功,读取响应内容
String payload = http.getString();
// 解析 JSON 数据(分配 256 字节内存,足够当前用途)
DynamicJsonDocument doc(256);
DeserializationError error = deserializeJson(doc, payload);
if (!error && doc.containsKey("results")) {
// 成功解析,提取所需字段
JsonObject now = doc["results"][0]["now"];
weatherText = now["text"].as<String>(); // 天气描述(英文)
temperature = now["temperature"].as<String>(); // 温度
String lastUpdate = doc["results"][0]["last_update"].as<String>();
dateStr = extractDate(lastUpdate); // 提取日期
} else {
// JSON 解析失败
weatherText = "解析错误";
temperature = "--";
dateStr = "----/--/--";
}
} else {
// HTTP 请求失败(如 403、404 等)
weatherText = "HTTP " + String(httpResponseCode);
temperature = "--";
dateStr = "----/--/--";
}
// 关闭 HTTP 连接
http.end();
}
void loop() {
// 每 10 分钟(600,000 毫秒)更新一次天气
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 600000) {
fetchWeather();
lastUpdate = millis();
}
// 清空屏幕并开始绘制新内容
display.clearDisplay();
display.setTextSize(2); // 默认使用大号字体
display.setTextColor(WHITE);
// 第一行:显示日期(例如:2025-04-05)
display.setCursor(0, 0);
display.print(dateStr);
// 第二行:显示城市名“北京”(使用点阵字模)
display.drawBitmap(48, 24, bei_text, 16, 16, 1); // “北” 显示在 (48,24)
display.drawBitmap(64, 24, jing_text, 16, 16, 1); // “京” 显示在 (64,24)
// 第三行:显示温度 + ℃ 符号
display.setCursor(0, 48);
display.print(temperature); // 先打印温度数字
// 计算温度字符串的宽度,以便将 ℃ 符号紧随其后
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(temperature, 0, 48, &x1, &y1, &w, &h);
display.drawBitmap(w + 4, 48, du_text, 16, 16, 1); // ℃ 显示在数字右侧
// 显示天气状态:
if (weatherText == "Clear") {
// 如果是晴天,显示“晴”字图标
display.drawBitmap(96, 48, qing_text, 16, 16, 1);
} else {
// 其他天气(如 Cloudy, Rain 等),显示英文文本
display.setTextSize(2); // 切换为小号字体
display.setCursor(90, 52); // 调整 Y 坐标使文字垂直居中
display.print(weatherText);
}
// 将缓冲区内容推送到 OLED 屏幕
display.display();
// 每 5 秒刷新一次屏幕(避免频繁刷新)
delay(5000);
}