ESP32S3+和风天气API实战:5分钟搞定天气预报显示(附完整代码)

📅 发布时间:2026/7/16 16:32:37 👁️ 浏览次数:
ESP32S3+和风天气API实战:5分钟搞定天气预报显示(附完整代码)
ESP32-S3气象站从零构建一个智能天气显示终端最近在捣鼓ESP32-S3发现这玩意儿真是个宝藏芯片。双核、蓝牙5.0、Wi-Fi 6还有丰富的IO口和足够的内存做个小型的物联网设备再合适不过了。正好手头有个闲置的小屏幕就想着能不能做个放在桌面的天气显示终端实时显示本地的天气情况。这个想法其实挺实用的——每天早上看一眼就知道今天要不要带伞穿什么衣服合适。而且ESP32-S3的功耗控制得不错插着USB电源就能一直运行比手机APP方便多了。我选择了和风天气的API主要是因为它对开发者比较友好免费额度足够个人使用数据更新也及时。整个项目从硬件连接到代码编写再到最后的调试优化大概花了一个周末的时间。下面我就把整个过程详细记录下来包括踩过的坑和找到的解决方案希望能帮到也想做类似项目的朋友。1. 硬件准备与环境搭建1.1 选择合适的ESP32-S3开发板市面上ESP32-S3的开发板选择很多从几十块到几百块的都有。我用的是一款带8MB PSRAM和8MB Flash的版本型号是ESP32-S3-DevKitC-1。选择这个型号主要是考虑到8MB PSRAM处理JSON数据和网络缓冲更从容USB-C接口连接更稳定供电也方便足够的GPIO方便后续扩展传感器或屏幕如果你手头有其他型号的ESP32-S3只要内存不少于4MBFlash不少于4MB基本上都能跑起来。不过要注意有些廉价版本可能没有外置PSRAM处理大量网络数据时可能会比较吃力。提示购买开发板时建议选择带有自动下载电路CH340C或CP2102的版本这样刷写固件时不需要手动按复位键。1.2 PlatformIO开发环境配置PlatformIO是我最喜欢的嵌入式开发环境之一相比Arduino IDE它在库管理和项目结构上更加专业。下面是在VSCode中配置PlatformIO的步骤首先确保你已经安装了Visual Studio Code然后通过扩展市场搜索并安装PlatformIO IDE。安装完成后你会看到左侧边栏多了一个蚂蚁图标那就是PlatformIO。创建新项目时有几个关键设置需要注意[env:esp32-s3-devkitc-1] platform espressif32 board esp32-s3-devkitc-1 framework arduino monitor_speed 115200 lib_deps bblanchon/ArduinoJson^6.21.3 links2004/WebSockets^2.3.6这个platformio.ini配置文件有几个要点board必须选择正确的板型否则编译会出错framework使用Arduino框架兼容性最好monitor_speed串口监视器波特率ESP32-S3默认是115200lib_deps这里预定义了项目需要的库PlatformIO会自动下载1.3 必要的软件库安装除了PlatformIO自带的ESP32支持包我们还需要几个关键的库ArduinoJson用于解析和风天气API返回的JSON数据WiFiESP32-S3内置但需要正确配置HTTPClient用于发送HTTP请求在PlatformIO中安装库非常简单打开PIO Home → Libraries搜索并安装即可。或者直接在platformio.ini文件中通过lib_deps指定版本PlatformIO会在编译时自动下载。安装完成后可以通过下面的命令验证库是否安装成功# 在PlatformIO CLI中执行 pio lib list你应该能看到类似这样的输出ArduinoJson 6.21.3 WiFi 2.0.0 HTTPClient 2.0.02. 和风天气API深度解析2.1 API申请与配置细节和风天气的免费套餐对于个人项目完全够用每天有1000次调用额度更新频率也能达到每小时一次。申请API Key的流程很简单访问和风天气开发者平台注册账号进入控制台创建新项目选择免费套餐获取你的API Key这里有个小技巧创建项目时应用名称可以填ESP32天气显示应用场景选个人学习这样审核通过更快。拿到API Key后建议先通过浏览器测试一下接口是否正常。比如获取深圳的3天天气预报https://devapi.qweather.com/v7/weather/3d?location101280601key你的API_Key如果返回的是JSON格式的天气数据说明API配置正确。如果返回错误信息常见的几种情况是401错误API Key无效或过期404错误location参数错误429错误请求频率超限2.2 理解API返回的数据结构和风天气的3天预报API返回的数据结构比较清晰我们主要关注daily数组中的这几个字段字段名数据类型说明示例值fxDatestring预报日期2024-01-15tempMaxint最高温度25tempMinint最低温度18iconDayint白天天气图标代码100iconNightint夜间天气图标代码150textDaystring白天天气文字描述晴textNightstring夜间天气文字描述晴其中天气图标代码是最有用的我们可以根据这个代码在ESP32-S3上显示对应的天气图标。和风天气提供了完整的天气代码表从100晴到511重度霾都有定义。2.3 城市Location ID的获取和风天气使用Location ID来标识城市而不是直接使用城市名称。获取Location ID有几种方法通过官方城市查询APIhttps://geoapi.qweather.com/v2/city/lookup?location深圳key你的API_Key下载城市列表文件和风天气提供了包含所有城市ID的CSV文件使用在线查询工具一些第三方网站提供了可视化查询对于固定地点的项目我建议把Location ID硬编码在代码中这样可以减少一次API调用。如果是需要切换地点的项目可以结合GPS模块或让用户手动输入。3. 核心代码实现与优化3.1 WiFi连接模块的健壮性设计WiFi连接是物联网设备最基础的环节也是最容易出问题的地方。我设计了一个带重试和错误处理的WiFi连接模块class WiFiManager { private: const char* ssid; const char* password; int maxRetries; int retryDelay; public: WiFiManager(const char* ssid, const char* password, int maxRetries 10, int retryDelay 500) : ssid(ssid), password(password), maxRetries(maxRetries), retryDelay(retryDelay) {} bool connect() { Serial.print(Connecting to WiFi: ); Serial.println(ssid); WiFi.begin(ssid, password); for (int i 0; i maxRetries; i) { if (WiFi.status() WL_CONNECTED) { Serial.println(\nWiFi connected!); Serial.print(IP address: ); Serial.println(WiFi.localIP()); return true; } Serial.print(.); delay(retryDelay); } Serial.println(\nWiFi connection failed!); return false; } void disconnect() { WiFi.disconnect(); Serial.println(WiFi disconnected); } bool isConnected() { return WiFi.status() WL_CONNECTED; } };这个设计有几个优点可配置的重试机制避免因网络波动导致的连接失败清晰的日志输出方便调试时查看连接状态封装良好在主程序中只需几行代码就能完成连接3.2 天气数据获取与解析获取天气数据的核心是HTTP请求和JSON解析。我创建了一个WeatherFetcher类来封装这些功能#include ArduinoJson.h #include HTTPClient.h class WeatherFetcher { private: String apiKey; String locationId; HTTPClient http; // 缓存上次获取的数据和时间 WeatherData cachedData; unsigned long lastFetchTime 0; const unsigned long cacheDuration 3600000; // 1小时缓存 public: WeatherFetcher(const String apiKey, const String locationId) : apiKey(apiKey), locationId(locationId) {} bool fetchWeather(WeatherData data) { // 检查缓存是否有效 if (millis() - lastFetchTime cacheDuration cachedData.isValid) { data cachedData; return true; } String url https://devapi.qweather.com/v7/weather/3d; url ?location locationId; url key apiKey; http.begin(url); http.addHeader(Accept-Encoding, gzip); // 启用压缩减少流量 int httpCode http.GET(); if (httpCode HTTP_CODE_OK) { String payload http.getString(); // 解析JSON DynamicJsonDocument doc(2048); DeserializationError error deserializeJson(doc, payload); if (!error) { // 提取今天的数据 JsonObject today doc[daily][0]; data.date today[fxDate].asString(); data.tempMax today[tempMax].asint(); data.tempMin today[tempMin].asint(); data.weatherDay today[textDay].asString(); data.weatherNight today[textNight].asString(); data.iconDay today[iconDay].asint(); data.iconNight today[iconNight].asint(); data.isValid true; // 更新缓存 cachedData data; lastFetchTime millis(); http.end(); return true; } } http.end(); return false; } };这里有几个关键点启用Gzip压缩和风天气API支持gzip压缩可以显著减少数据传输量数据缓存避免频繁调用API节省调用次数错误处理网络错误和JSON解析错误都需要处理3.3 内存优化与稳定性考虑ESP32-S3虽然有8MB PSRAM但在长时间运行的项目中内存管理仍然很重要// 使用PSRAM如果可用 #if CONFIG_SPIRAM_SUPPORT heap_caps_malloc_extmem_enable(512); // 启用外部RAM #endif // 优化JSON文档大小 DynamicJsonDocument doc(1024); // 根据实际数据大小调整 // 使用String时的注意事项 void processWeatherData(const String jsonStr) { // 避免在循环中创建String对象 StaticJsonDocument512 filter; filter[daily][0][tempMax] true; filter[daily][0][tempMin] true; filter[daily][0][textDay] true; deserializeJson(doc, jsonStr, DeserializationOption::Filter(filter)); }内存使用的最佳实践尽量使用栈分配而不是堆分配及时释放不再使用的对象使用String时注意避免内存碎片定期检查内存使用情况4. 显示界面设计与用户体验4.1 选择适合的显示方案根据你的需求可以选择不同的显示方案方案一OLED显示屏128x64优点功耗低显示清晰适合简单信息缺点屏幕小显示内容有限适用场景桌面小摆件只需要显示温度天气方案二TFT LCD240x320优点色彩丰富可以显示图标和更多信息缺点功耗较高需要更多GPIO适用场景需要显示详细天气信息和图标方案三电子墨水屏E-Ink优点超低功耗阳光下可视性好缺点刷新率低价格较高适用场景需要长时间显示且不频繁更新的场合我选择了1.3寸的TFT LCD分辨率240x240通过SPI接口连接。这个尺寸足够显示完整的天气信息而且价格适中。4.2 天气图标的实现和风天气提供了天气代码但没有提供图标资源。我们需要自己创建或寻找合适的图标集。我选择使用开源的气象图标并转换为适合ESP32-S3显示的格式// 天气图标定义简化版实际需要位图数据 const uint16_t WEATHER_ICONS[][8] { // 晴天图标100 {0x0000, 0x0420, 0x0C30, 0x1C38, 0x3C3C, 0x1C38, 0x0C30, 0x0420}, // 多云图标101 {0x0000, 0x0C30, 0x1E78, 0x3FFC, 0x7FFE, 0x7FFE, 0x3FFC, 0x1E78}, // 雨天图标300 {0x0000, 0x0420, 0x0C30, 0x1C38, 0x1818, 0x3C3C, 0x6666, 0xC3C3} }; void drawWeatherIcon(TFT_eSPI tft, int x, int y, int weatherCode) { int iconIndex 0; // 根据天气代码选择图标 if (weatherCode 100 weatherCode 104) { iconIndex 0; // 晴 } else if (weatherCode 150 weatherCode 153) { iconIndex 1; // 夜间晴 } else if (weatherCode 300 weatherCode 399) { iconIndex 2; // 雨 } // ... 其他天气代码 // 绘制图标 for (int i 0; i 8; i) { for (int j 0; j 8; j) { if (WEATHER_ICONS[iconIndex][i] (1 j)) { tft.drawPixel(x j, y i, TFT_WHITE); } } } }对于更复杂的图标建议使用图像转换工具将PNG图片转换为C数组然后存储在SPIFFS文件系统中。4.3 界面布局与信息展示一个好的天气显示界面应该清晰易读信息层次分明。我的布局设计如下┌─────────────────────────────┐ │ 深圳天气 │ │ 2024-01-15 周一 │ ├─────────────────────────────┤ │ ☀️ │ │ 晴 │ │ 18°C ~ 25°C │ ├─────────────────────────────┤ │ 湿度65% 风速3级 │ │ 日出06:45 日落17:30 │ └─────────────────────────────┘实现这个界面的代码void drawWeatherScreen(TFT_eSPI tft, const WeatherData data) { tft.fillScreen(TFT_BLACK); // 标题栏 tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); tft.setCursor(10, 10); tft.print(深圳天气); // 日期 tft.setTextSize(1); tft.setCursor(10, 40); tft.print(data.date); // 天气图标居中 int iconX (tft.width() - 64) / 2; drawWeatherIcon(tft, iconX, 60, data.iconDay); // 天气描述 tft.setTextSize(2); tft.setCursor(10, 130); tft.print(data.weatherDay); // 温度大字体 tft.setTextSize(3); char tempStr[20]; sprintf(tempStr, %d°C ~ %d°C, data.tempMin, data.tempMax); int tempWidth tft.textWidth(tempStr); int tempX (tft.width() - tempWidth) / 2; tft.setCursor(tempX, 160); tft.print(tempStr); // 底部信息栏 tft.drawLine(0, 200, tft.width(), 200, TFT_GRAY); tft.setTextSize(1); tft.setCursor(10, 210); tft.print(最后更新); // 显示更新时间 char timeStr[20]; sprintf(timeStr, %02d:%02d, hour(), minute()); tft.print(timeStr); }4.4 自动更新与低功耗优化为了让设备能够7x24小时运行需要考虑功耗和稳定性class WeatherStation { private: WeatherFetcher fetcher; Display display; WiFiManager wifi; unsigned long lastUpdateTime 0; const unsigned long updateInterval 3600000; // 1小时更新一次 // 深度睡眠相关 #ifdef USE_DEEP_SLEEP const unsigned long sleepDuration 3540000; // 59分钟睡眠 #endif public: WeatherStation(WeatherFetcher f, Display d, WiFiManager w) : fetcher(f), display(d), wifi(w) {} void run() { unsigned long currentTime millis(); // 检查是否需要更新 if (currentTime - lastUpdateTime updateInterval) { updateWeather(); lastUpdateTime currentTime; } // 其他任务... display.update(); // 更新显示如果有动画 #ifdef USE_DEEP_SLEEP // 进入深度睡眠节省功耗 if (shouldSleep()) { enterDeepSleep(); } #endif } void updateWeather() { if (!wifi.isConnected()) { if (!wifi.connect()) { Serial.println(Failed to connect WiFi, skipping update); return; } } WeatherData data; if (fetcher.fetchWeather(data)) { display.showWeather(data); Serial.println(Weather updated successfully); } else { Serial.println(Failed to fetch weather data); } // 更新完成后断开WiFi节省功耗 wifi.disconnect(); } #ifdef USE_DEEP_SLEEP void enterDeepSleep() { Serial.println(Entering deep sleep); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); // 配置唤醒定时器 esp_sleep_enable_timer_wakeup(sleepDuration * 1000); esp_deep_sleep_start(); } #endif };这个设计实现了定时更新每小时更新一次天气数据智能连接只在需要时连接WiFi深度睡眠大幅降低功耗可选错误恢复网络失败时自动重试5. 高级功能扩展与实践5.1 多城市天气切换如果你需要显示多个城市的天气可以扩展代码支持城市切换class MultiCityWeather { private: struct CityInfo { String name; String locationId; WeatherData data; unsigned long lastUpdate; }; std::vectorCityInfo cities; int currentCityIndex 0; public: void addCity(const String name, const String locationId) { CityInfo city; city.name name; city.locationId locationId; city.lastUpdate 0; cities.push_back(city); } void switchToNextCity() { currentCityIndex (currentCityIndex 1) % cities.size(); updateCurrentCity(); } void updateCurrentCity() { if (cities.empty()) return; CityInfo city cities[currentCityIndex]; unsigned long currentTime millis(); // 如果数据超过1小时重新获取 if (currentTime - city.lastUpdate 3600000) { WeatherFetcher fetcher(apiKey, city.locationId); if (fetcher.fetchWeather(city.data)) { city.lastUpdate currentTime; } } } const WeatherData getCurrentWeather() const { static WeatherData emptyData; if (cities.empty()) return emptyData; return cities[currentCityIndex].data; } const String getCurrentCityName() const { static String empty; if (cities.empty()) return empty; return cities[currentCityIndex].name; } };使用示例MultiCityWeather weather; weather.addCity(深圳, 101280601); weather.addCity(北京, 101010100); weather.addCity(上海, 101020100); // 每10秒切换一个城市 void loop() { static unsigned long lastSwitch 0; if (millis() - lastSwitch 10000) { weather.switchToNextCity(); display.showWeather(weather.getCurrentWeather(), weather.getCurrentCityName()); lastSwitch millis(); } }5.2 天气预警与通知和风天气还提供天气预警API可以获取当地的天气预警信息class WeatherAlert { public: struct AlertInfo { String title; // 预警标题 String type; // 预警类型 String level; // 预警等级 String text; // 预警内容 time_t startTime; // 开始时间 time_t endTime; // 结束时间 }; bool fetchAlerts(const String locationId, std::vectorAlertInfo alerts) { String url https://devapi.qweather.com/v7/warning/now; url ?location locationId; url key apiKey; HTTPClient http; http.begin(url); int httpCode http.GET(); if (httpCode HTTP_CODE_OK) { String payload http.getString(); DynamicJsonDocument doc(2048); DeserializationError error deserializeJson(doc, payload); if (!error doc.containsKey(warning)) { JsonArray warningArray doc[warning]; for (JsonObject warning : warningArray) { AlertInfo alert; alert.title warning[title].asString(); alert.type warning[type].asString(); alert.level warning[level].asString(); alert.text warning[text].asString(); // 解析时间 String start warning[pubTime].asString(); alert.startTime parseTime(start); alerts.push_back(alert); } http.end(); return true; } } http.end(); return false; } private: time_t parseTime(const String timeStr) { // 解析时间字符串格式2024-01-15T14:30:0008:00 struct tm tm; strptime(timeStr.c_str(), %Y-%m-%dT%H:%M:%S, tm); return mktime(tm); } };当检测到天气预警时可以在显示屏上用醒目的颜色显示或者通过蜂鸣器发出警报。5.3 数据本地存储与历史记录有时候我们需要查看历史天气数据或者在网络不可用时显示上次的数据#include SPIFFS.h class WeatherStorage { private: const char* dataFile /weather_data.json; public: bool saveWeatherData(const WeatherData data) { if (!SPIFFS.begin(true)) { Serial.println(Failed to mount SPIFFS); return false; } File file SPIFFS.open(dataFile, FILE_WRITE); if (!file) { Serial.println(Failed to open file for writing); return false; } DynamicJsonDocument doc(512); doc[date] data.date; doc[tempMax] data.tempMax; doc[tempMin] data.tempMin; doc[weatherDay] data.weatherDay; doc[iconDay] data.iconDay; doc[timestamp] millis(); serializeJson(doc, file); file.close(); Serial.println(Weather data saved); return true; } bool loadWeatherData(WeatherData data) { if (!SPIFFS.begin(true)) { return false; } if (!SPIFFS.exists(dataFile)) { return false; } File file SPIFFS.open(dataFile, FILE_READ); if (!file) { return false; } DynamicJsonDocument doc(512); DeserializationError error deserializeJson(doc, file); file.close(); if (error) { return false; } data.date doc[date].asString(); data.tempMax doc[tempMax].asint(); data.tempMin doc[tempMin].asint(); data.weatherDay doc[weatherDay].asString(); data.iconDay doc[iconDay].asint(); data.isValid true; // 检查数据是否过期超过24小时 unsigned long savedTime doc[timestamp].asunsigned long(); if (millis() - savedTime 86400000) { // 24小时 data.isValid false; } return data.isValid; } };这样即使网络暂时不可用设备也能显示上次获取的天气数据提高了用户体验。5.4 远程配置与OTA更新对于部署在固定位置的设备远程管理和更新功能非常有用#include WebServer.h #include Update.h class DeviceManager { private: WebServer server; String wifiSSID; String wifiPassword; String weatherAPIKey; String locationId; public: void setupWebServer() { server.on(/, HTTP_GET, [this]() { server.send(200, text/html, configPage()); }); server.on(/config, HTTP_POST, [this]() { if (server.hasArg(ssid)) wifiSSID server.arg(ssid); if (server.hasArg(password)) wifiPassword server.arg(password); if (server.hasArg(apikey)) weatherAPIKey server.arg(apikey); if (server.hasArg(location)) locationId server.arg(location); saveConfig(); server.send(200, text/plain, Configuration saved, restarting...); delay(1000); ESP.restart(); }); server.on(/update, HTTP_POST, [this]() { server.sendHeader(Connection, close); server.send(200, text/plain, (Update.hasError()) ? FAIL : OK); ESP.restart(); }, [this]() { HTTPUpload upload server.upload(); if (upload.status UPLOAD_FILE_START) { Serial.printf(Update: %s\n, upload.filename.c_str()); if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { Update.printError(Serial); } } else if (upload.status UPLOAD_FILE_WRITE) { if (Update.write(upload.buf, upload.currentSize) ! upload.currentSize) { Update.printError(Serial); } } else if (upload.status UPLOAD_FILE_END) { if (Update.end(true)) { Serial.printf(Update Success: %u\nRebooting...\n, upload.totalSize); } else { Update.printError(Serial); } } }); server.begin(); Serial.println(HTTP server started); } void handleClient() { server.handleClient(); } private: String configPage() { return R( !DOCTYPE html html head titleWeather Station Config/title meta nameviewport contentwidthdevice-width, initial-scale1 /head body h1Weather Station Configuration/h1 form action/config methodpost labelWiFi SSID:/labelbr input typetext namessid value) wifiSSID R(brbr labelWiFi Password:/labelbr input typepassword namepassword value) wifiPassword R(brbr labelWeather API Key:/labelbr input typetext nameapikey value) weatherAPIKey R(brbr labelLocation ID:/labelbr input typetext namelocation value) locationId R(brbr input typesubmit valueSave /form hr h2Firmware Update/h2 form methodPOST action/update enctypemultipart/form-data input typefile nameupdate input typesubmit valueUpdate /form /body /html ); } void saveConfig() { // 保存配置到SPIFFS File configFile SPIFFS.open(/config.json, w); if (configFile) { DynamicJsonDocument doc(256); doc[wifi_ssid] wifiSSID; doc[wifi_password] wifiPassword; doc[weather_apikey] weatherAPIKey; doc[location_id] locationId; serializeJson(doc, configFile); configFile.close(); } } };这个Web配置界面允许用户通过浏览器修改WiFi设置、API密钥等配置还可以上传新的固件进行OTA更新。6. 项目部署与维护建议6.1 硬件组装注意事项组装硬件时需要注意以下几点电源稳定性ESP32-S3对电源质量比较敏感建议使用质量好的USB电源适配器信号干扰WiFi天线尽量远离金属物体和电源线散热考虑如果放在封闭外壳内确保有足够的散热孔防静电触摸电子元件前先释放静电我使用的连接方式ESP32-S3 -- TFT LCD (SPI接口) 引脚分配 - GPIO 18: TFT_SCK - GPIO 23: TFT_MOSI - GPIO 5: TFT_CS - GPIO 4: TFT_DC - GPIO 2: TFT_RST - GPIO 15: TFT_BL (背光控制)6.2 软件配置与调试技巧部署时可能会遇到的一些常见问题及解决方法问题1WiFi连接不稳定// 增加重试次数和超时时间 WiFi.setSleep(false); // 禁用WiFi睡眠 WiFi.setTxPower(WIFI_POWER_19_5dBm); // 提高发射功率问题2内存不足// 监控内存使用 void checkMemory() { Serial.printf(Free heap: %d\n, ESP.getFreeHeap()); Serial.printf(Min free heap: %d\n, ESP.getMinFreeHeap()); Serial.printf(Max alloc heap: %d\n, ESP.getMaxAllocHeap()); }问题3天气数据更新失败// 实现备用数据源 bool fetchWeatherWithFallback(WeatherData data) { if (!fetchFromPrimaryAPI(data)) { Serial.println(Primary API failed, trying fallback...); return fetchFromSecondaryAPI(data); } return true; }6.3 长期运行稳定性优化为了确保设备能够长期稳定运行定期重启每周自动重启一次清理内存碎片看门狗定时器防止程序卡死错误日志将错误信息保存到SPIFFS方便排查自动恢复网络异常时自动重连// 看门狗定时器 #include esp_task_wdt.h void setupWatchdog() { esp_task_wdt_init(30, true); // 30秒超时 esp_task_wdt_add(NULL); // 添加当前任务到看门狗 } void feedWatchdog() { esp_task_wdt_reset(); // 喂狗 } // 在loop中定期调用 void loop() { feedWatchdog(); // ... 其他代码 }6.4 成本控制与扩展建议这个项目的硬件成本可以控制在100元以内组件型号参考价格备注ESP32-S3开发板ESP32-S3-DevKitC-145元带8MB PSRAMTFT显示屏1.3寸IPS25元SPI接口240x240分辨率外壳3D打印或亚克力15元可选电源USB充电器10元5V/1A其他杜邦线等5元连接线材如果想要进一步扩展功能可以考虑添加传感器温湿度传感器、气压传感器语音输出通过喇叭播报天气物理按钮切换城市、刷新数据电池供电配合太阳能板实现完全无线整个项目最耗时的部分其实是界面设计和用户体验优化。我前后调整了三次显示布局才找到既美观又实用的方案。另外天气图标的绘制也花了不少时间需要找到合适的图标资源并转换为单片机可用的格式。在实际使用中我发现ESP32-S3的性能完全足够处理天气数据甚至还有余力跑一些简单的动画效果。和风天气的API响应速度很快通常200-300ms就能返回数据用户体验很好。如果你也想做一个类似的天气显示终端建议先从最简单的版本开始只显示温度和天气状况等基本功能稳定后再逐步添加更多特性。这样即使遇到问题也容易定位和解决。最重要的是享受制作的过程看着自己做的设备每天为你提供有用的信息那种成就感是买现成产品无法比拟的。