petrochen/esp8266-weather-clock-opensource

GitHub: petrochen/esp8266-weather-clock-opensource

针对 AliExpress 上存在严重安全漏洞的 ESP8266 天气时钟,逆向工程后重写的安全开源固件,修复了 WiFi 密码明文泄露等问题并增加了 OTA 更新等功能。

Stars: 9 | Forks: 5

# 逆向工程一款 5 欧元的 AliExpress 天气时钟:一个安全故事

Release License Issues Hardware Status

## 太长不看(TL;DR) 我从 AliExpress 买了一款可爱的天气时钟套件([TJ-56-654](https://pt.aliexpress.com/item/1005008333782531.html)),并发现它会以**明文形式向无线电波范围内的任何人泄露我的 WiFi 密码**。因此,我提取了固件,编写了自己的固件,最终得到了一个完全异步、支持 OTA 更新、适配 Home Assistant 且真正安全的智能时钟。 ## 目录 - [发现之旅:当“智能”意味着“不安全”](#the-discovery-when-smart-means-insecure) - [设备简介](#the-device) - [深入调查](#the-investigation) - [解决方案:自定义固件](#the-solution-custom-firmware) - [技术深入解析](#technical-deep-dive) - [演进历程:v1.7 → v1.9.4](#the-journey-v17--v194) - [未来计划:Home Assistant 集成](#whats-next-home-assistant-integration) - [如何刷入此固件](#how-to-flash-this-firmware) - [Web 界面](#web-interface) - [API 文档](#api-documentation) - [安全性改进](#security-improvements) - [经验教训](#lessons-learned) - [致谢](#credits) ## 发现之旅:当“智能”意味着“不安全” 事情起初很简单。我买了一个看起来很有趣的 DIY 电子项目:一个基于 ESP8266 的天气时钟,带有透明亚克力外壳和 OLED 显示屏。商品详情页承诺: - ✅ WiFi 天气更新 - ✅ 3 天天气预报 - ✅ 温度、湿度、日期/时间 - ✅ “智能连接 WIFI” 但他们没有提到: **🚨 严重安全漏洞 🚨** 当你首次设置设备时,它会创建一个带有默认密码的接入点(AP)。这很公平——这正是 WiFiManager 的工作方式。但糟糕的在后头: 1. 你连接到 AP(192.168.4.1) 2. 配置你的家庭 WiFi 凭据 3. 设备连接到你的网络 4. **开放的 AP 保持并行激活状态** 5. **你的 WiFi 密码以明文形式显示在配置页面上** 任何在 WiFi 范围内的人都可以: - 连接到设备的 AP(使用弱默认密码) - 浏览至 192.168.4.1 - 读取你的明文 WiFi 密码 - 访问你的网络 这是糟糕的物联网(IoT)安全设计的教科书级案例。我果断拒绝。 ## 设备简介 **产品**:ESP8266 迷你天气时钟套件 **型号**:TJ-56-654 **价格**:~5 欧元 **来源**:[AliExpress 链接](https://pt.aliexpress.com/item/1005008333782531.html) ### 原始硬件规格 | 组件 | 详情 | | -------- | --------------------------------------- | | **MCU** | ESP-01S (ESP8266EX, 1MB flash, 80KB RAM) | | **屏幕** | GM009605v4.3 OLED (128x64, I2C) | | **电源** | 5V USB (Micro-USB) | | **外壳** | 透明亚克力 (40x40x43mm) | | **PCB** | TJ-56-654 主板 | ### 包装内容 - 亚克力外壳零件(6 件) - ESP-01S WiFi 模块 - OLED 显示模块 - 带有排针的主 PCB - USB 电源线 - 黄铜六角柱和螺丝 - 排针(需焊接) ### 原始固件问题 除了密码泄露之外: - **依赖 QWeather API**:需要注册账户、创建项目、管理 API 密钥 - **中国云服务**:所有天气数据通过专有服务器路由 - **无 OTA 更新**:固件更新需要拆解并连接 FTDI - **功能有限**:固定显示模式,无法自定义 - **未知代码**:闭源固件,无法审计其行为 ## 深入调查 ### 拆解 透明外壳使检查变得容易——只需拧下黄铜支柱即可。内部包含: - **ESP-01S 模块**,清楚地标有引脚分配 - **I2C OLED 显示屏**,通过 4 个引脚(VCC、GND、SDA、SCL)连接 - **无额外传感器**(温度/湿度来自天气 API,非本地测量) ESP-01S 的引脚分配直接印在 PCB 上: ``` 3V3 | GND TX | GPIO0 (I2C SDA) RX | GPIO2 (I2C SCL) EN | GND ``` ### 连接 FTDI 要刷入自定义固件,你需要: 1. **FTDI USB 转串口适配器**(3.3V!不是 5V - 否则会烧毁 ESP8266) 2. **杜邦线** 3. **稳定的双手** **接线:** ``` FTDI ESP-01S ──────────────────── 3V3 → 3V3 GND → GND TX → RX RX → TX GND → GPIO0 (for programming mode) ``` **进入刷机模式:** 1. 将 GPIO0 连接到 GND 2. 给设备上电 3. 启动后移除 GPIO0 到 GND 的连接 4. 设备现在处于编程模式 **编程:** - 使用支持 ESP8266 的 Arduino IDE - 选择开发板:“Generic ESP8266 Module” - Flash 大小:1MB (FS:64KB OTA:~470KB) - 上传速度:115200 baud 首次刷入支持 OTA 的固件后,就再也不需要线缆了——所有更新都通过 WiFi 进行。 ## 解决方案:自定义固件 我决定编写一个完整的替换固件,具备以下特点: ### 核心原则 1. **安全第一**:无硬编码凭据,无开放网络,使用带有正确 AP 超时的 WiFiManager 2. **隐私保护**:使用免费、开放的 API(使用 Open-Meteo 替代 QWeather) 3. **可维护性**:OTA 更新带来轻松的改进体验 4. **性能**:完全异步架构,无阻塞操作 5. **可靠性**:正确的错误处理、指数退避、内存安全 ### 已实现功能 #### 🌐 网络与时间 - **WiFiManager** 捕获门户,用于安全的首次设置 - **混合 WiFi**:启动时同步(确保正确初始化),运行时异步重连 - **NTP 时间同步**,具有可配置的服务器和间隔 - **时区支持**,带有自动欧洲夏令时计算 - **mDNS**:可通过 `http://tj56654-clock.local/` 访问 #### 🌦️ 天气数据 - **Open-Meteo API**:免费,无需注册,无需 API 密钥 - **可配置位置**:纬度/经度 + 城市名称 - **数据**:温度、日出、日落、日照时长 - **智能更新**:异步获取,每 30 分钟一次(可配置) #### 🔄 OTA 更新 - **基于 Web 的 OTA**:在浏览器中通过 `/update` 上传 .bin 文件 - **ArduinoOTA**:直接从 Arduino IDE 更新 - **非阻塞**:系统在更新期间保持响应 - **安全**:受密码保护的上传(admin/admin - 请务必修改!) #### 📺 显示模式 三个轮换显示屏幕(可配置间隔): 1. **时间模式** - 大号 HH:MM 显示 - 闪烁的冒号动画 - 星期和日期 - 支持 12/24 小时制 2. **天气模式** - 带有上标 °c 的温度 - 城市名称 - 简洁的极简布局 3. **日出/日落模式** - 带有 ↑ 箭头的日出时间 - 带有 ↓ 箭头的日落时间 - **日照时长**(例如,“Day 9h 41m”) 所有模式均居中对齐、支持旋转,并能优雅地处理数据缺失。 #### 🌐 Web 界面 - `/` - 显示实时时间的主页 - `/config` - 完整的配置表单 - `/debug` - 系统诊断 - `/update` - OTA 固件上传 #### 🔌 REST API 所有端点均返回 JSON: - `GET /api/time` - 当前时间 - `GET /api/status` - 系统状态(WiFi、运行时间、堆内存) - `GET /api/debug` - 详细诊断 - `GET /api/weather` - 天气 + 日出/日落 - `GET /api/config` - 导出配置 - `POST /api/config` - 导入配置 - `POST /api/eeprom-clear` - 恢复出厂设置 - `POST /api/reboot` - 远程重启 ## 技术深入解析 ### 架构:完全异步状态机 该固件在主循环中使用**零阻塞操作**。一切均基于状态机: #### 天气状态机 ``` enum WeatherState { IDLE, REQUESTING, SUCCESS, FAILED }; ``` 使用 `AsyncHTTPRequest` 库: - 非阻塞 HTTP 请求 - 基于回调的响应处理 - 失败时的指数退避(1s → 2s → 4s) - 放弃前最多重试 3 次 #### NTP 状态机 ``` enum NTPState { IDLE, REQUEST_SENT, WAITING, SUCCESS, FAILED }; ``` 自定义的手动 NTP 实现: - 构建原始 UDP 数据包(48 字节) - 非阻塞的 `parsePacket()` 检查 - 5 秒超时 - 独立的 epoch 跟踪,确保两次同步间的准确性 #### WiFi 状态机 ``` enum WiFiConnectionState { IDLE, CONNECTING, CONNECTED, FAILED }; ``` **混合模型**(这至关重要!): - **Setup 阶段**:同步连接(等待最多 10 秒) - 为什么?OTA、Web 服务器、NTP 都需要 WiFi 准备就绪 - 否则,设备会显示 10 秒以上的黑屏 - **Loop 阶段**:异步重连(每 5 秒检查一次) - 为什么?在 WiFi 断开时不要冻结整个系统 ### 内存优化 ESP8266 具有严格的内存限制: | 内存类型 | 总计 | 已用 | 使用率 | 状态 | | -------- | --------- | ------- | ------- | ----------- | | **Flash** | 1,048,576 | 408,844 | 38% | ✅ 充足 | | **RAM** | 80,192 | 37,644 | 46% | ✅ 安全 | | **IRAM** | 65,536 | 61,987 | **94%** | ⚠️ 紧张 | **IRAM 危机解决方案:** IRAM(指令 RAM)有限且极易填满。解决方案是:`ICACHE_FLASH_ATTR` 宏。 ``` void ICACHE_FLASH_ATTR handleConfig() { // This function's code lives in Flash, not IRAM // Saves precious IRAM at cost of slightly slower execution } ``` 应用于 26 个函数(Web 处理程序、显示、配置实用程序),将 IRAM 压力从**溢出风险**降低到了**可持续的 94%**。 **字符串安全:** 避免在循环中使用 String 拼接(会导致堆内存碎片化): ``` // ❌ BAD - 140+ concatenations String html = ""; html += F(""); html += F("..."); // x138 more times // ✅ GOOD - Chunked responses server.setContentLength(CONTENT_LENGTH_UNKNOWN); server.send(200, "text/html", ""); server.sendContent_P(HTML_HEADER); server.sendContent_P(HTML_FOOTER); server.sendContent(""); // End ``` ### 配置存储 26 个字段的结构体存储在 EEPROM(512 字节)中: ``` struct Config { char ssid[32]; char password[64]; int timezone_offset; bool dst_enabled; uint8_t brightness; char ntp_server[64]; unsigned long ntp_interval; bool hour_format_24; char hostname[32]; float latitude; float longitude; char city_name[32]; bool weather_enabled; unsigned long weather_interval; unsigned long display_rotation_sec; bool show_weather; bool show_sunrise_sunset; uint8_t display_orientation; uint32_t magic; // 0xC10CC10C - validation }; ``` **EEPROM 验证**:魔术数(Magic number)检查可防止加载损坏的数据。验证失败时,会优雅地恢复为默认的安全配置。 ### 显示屏硬件发现 这花费了**3 次固件迭代**才弄对: **v1.5**:假设为 TM1637(7 段 LED 驱动器) - ❌ 错误 - 设备使用的是 OLED,而不是 7 段 LED **v1.6**:尝试 TM1650(另一种 LED 驱动器) - ❌ 错误 - I2C 地址不匹配 **v1.7**:确认为 GM009605v4.3(兼容 SSD1306 的 OLED) - ✅ 正确!使用了 Adafruit_SSD1306 库 - ✅ 发现了交换的引脚:SDA 在 GPIO0 上,SCL 在 GPIO2 上 **引脚映射怪癖:** 标准 ESP8266 I2C 使用 GPIO4 (SDA) 和 GPIO5 (SCL),但 ESP-01S 仅暴露了 GPIO0 和 GPIO2。电路板设计者映射为: - GPIO0 → SDA(不寻常) - GPIO2 → SCL(不寻常) 这与典型的开发板是**相反**的,但一旦配置好即可完美工作: ``` Wire.begin(0, 2); // SDA=GPIO0, SCL=GPIO2 ``` ## 演进历程:v1.7 → v1.9.4 ### v1.7:屏幕发现 ✅ - 识别出正确的显示硬件 - 基础时间显示工作正常 - WiFiManager 集成 - 首次 OTA 部署 ### v1.8:稳定性与安全性 🔒 **目标**:修复内存问题,消除安全漏洞 **更改:** - IRAM 优化( 26 个函数添加了 `ICACHE_FLASH_ATTR`) - 移除了硬编码的 WiFi 凭据 - 修复了 NTP 间隔错误(配置值被忽略) - 修复了 JSON 导入中的布尔值解析 - 添加了输入验证(缓冲区溢出保护) - 分块 HTTP 响应(消除了 140 多次 String 拼接) **结果**:IRAM 使用率 94% → 70%,无内存泄漏,安全的配置存储 ### v1.9.0:全面异步重构 ⚡ **目标**:消除所有阻塞操作 **更改:** - 异步 HTTP 天气获取(AsyncHTTPRequest 库) - 自定义异步 NTP 实现(手动 UDP 数据包) - 异步 WiFi 连接(状态机) - 从 `loop()` 中移除了所有 `delay()` 调用 - 指数退避重试逻辑 **性能:** | 操作 | 之前 (v1.8) | 之后 (v1.9.0) | 改进 | | ------------- | ------------- | -------------- | -------------------- | | 天气获取 | 1-10s 阻塞 | 0ms | ✅ 异步回调 | | NTP 同步 | 5-20s 阻塞 | 0ms | ✅ 非阻塞 UDP | | WiFi 重连 | 15s 阻塞 | 0ms | ✅ 状态机 | | Loop 循环时间 | 最少 10ms | <1ms | ✅ 快 10 倍 | **结果**:在获取天气数据或 OTA 更新期间设备保持响应! ### v1.9.1:混合模式修复 🎯 **发现的问题:** 部署 v1.9.0 后,启动时显示屏会**黑屏 10 秒钟**,并且日志中出现 `DNS resolution failed` 错误。 **根本原因:** 完全异步的 WiFi 破坏了**初始化顺序**: ``` void setup() { setupWiFi(); // Returns immediately (async) setupOTA(); // WiFi NOT ready! ❌ setupWebServer(); // WiFi NOT ready! ❌ testInternetConnectivity(); // WiFi NOT ready! → DNS error } ``` **解决方案:混合模型** | 阶段 | WiFi 模式 | 是否阻塞? | 为什么? | | -------- | ---------- | ---------- | ----------------------------- | | `setup()` | 同步 | 最多 10s | OTA/Web/NTP 需要 WiFi 准备就绪 | | `loop()` | 异步 | 0s | 重连时不要冻结系统 | **结果:** - ✅ WiFi 连接后显示屏立即显示时间(约 15 秒启动) - ✅ 没有“DNS resolution failed”错误 - ✅ 保证了正确的初始化顺序 - ✅ 运行期间 WiFi 丢失时设备永不冻结 ### v1.9.2:WiFi 弹性 🛡️ **发现的问题:** 在 WiFi 中断后,设备会**清除存储的凭据**并进入 AP 模式,每次路由器重启都需要手动重新配置。 **根本原因:** 连接失败时激进地清除凭据: ``` if (wifiRetry.currentRetry >= wifiRetry.maxRetries) { memset(config.ssid, 0, sizeof(config.ssid)); // ❌ Clears credentials! memset(config.password, 0, sizeof(config.password)); saveConfig(); // Enter AP mode... } ``` **解决方案:弹性 WiFi** | 特性 | 之前 (v1.9.1) | 之后 (v1.9.2) | | ------------------ | ------------------------ | ------------------------------ | | 凭据清除 | 失败 5 次后 | 绝不 | | 重试策略 | 5 次尝试后放弃 | 带退避的无限重试 | | 最大重试间隔 | N/A | 5 分钟 | | 备用 AP | 清除凭据后激活 | 约 5 分钟后(双 STA+AP 模式) | | 断网时的时钟 | 黑屏 | 显示上次同步的时间 | **关键更改:** - **绝不在**连接失败时清除凭据 - **指数退避**:5s → 10s → 20s → ... → 最大 5min - **备用 AP**("TJ56654-Setup")在约 5 分钟后启用,同时继续重试 - **双 STA+AP 模式**:AP 激活时设备继续重连尝试 - **SDK 凭据**:仅在首次启动时使用(无保存的 SSID);后续启动直接使用保存的凭据 - **“无 WiFi”显示**:显示重试倒计时而非神秘的数字 - **“!” 指示器**:WiFi 断开时显示在日期行中 **网络活动总结:** | 服务 | 间隔 | 端点 | 协议 | | ------- | --------- | ------------------ | ------------- | | NTP | 1 小时 | pool.ntp.org:123 | UDP | | 天气 | 30 分钟 | api.open-meteo.com | HTTP | | mDNS | 持续 | 224.0.0.251 | UDP multicast | 每天总共约 50 个请求。 **结果:** - ✅ 凭据在 WiFi 断网后依然保留 - ✅ WiFi 恢复后设备自动重连 - ✅ 时钟使用上次同步的时间继续运行 - ✅ 如有需要,用户可通过备用 AP 重新配置 **启动时间线:** ``` [0-5s] Display init, startup animation [5-15s] WiFi connection (SYNCHRONOUS in setup()) ✅ WiFi connected! IP assigned [15-20s] OTA init, web server start, NTP client ready ✅ Internet test: PASSED [20-30s] First async NTP sync ✅ Time synced and displayed ``` ### v1.9.3:模块化架构 🗂️ 将 2100 行的单体 `.ino` 文件拆分为专注的模块: | 文件 | 职责 | | ------------------- | -------------------------------------- | | `weather_clock.ino` | 入口点:`setup()` 和 `loop()` | | `config.h` | 配置结构体,EEPROM 布局,常量 | | `globals.h` | 共享状态和外部声明 | | `display.cpp` | OLED 渲染 | | `ntp_client.cpp` | 异步 NTP 同步 | | `weather.cpp` | Open-Meteo API 获取 | | `web_server.cpp` | Web UI 和 REST API | | `wifi_manager.cpp` | WiFi 连接和弹性重连 | ### v1.9.4:错误修复与清理 ✅(当前版本) 修复了社区报告的错误: - **日期时区** (#5):日期现在在本地午夜更改,而不是 UTC 午夜 - **天气刷新** (#7):首次获取后,定期的天气更新不再受阻 - **WiFi 热点** (#3):当存在已保存的 SSID 时,设备不再连接到开放的 SDK 缓存网络(例如公共热点),防止配置损坏 - **ArduinoJson v7**:更新 `StaticJsonDocument` → `JsonDocument` 以兼容库 - **编译器警告**:移除了未使用的变量,修复了 sprintf 缓冲区大小 ### 内存演变 | 版本 | RAM 使用率 | IRAM 使用率 | Flash 使用率 | 备注 | | ----- | -------------- | ----------------- | ------------- | -------------------- | | v1.7 | 34,980 (43%) | **61,987 (94%)** | 407,500 (38%) | IRAM 危机 | | v1.8 | 36,980 (46%) | **45,120 (68%)** | 407,800 (38%) | ICACHE_FLASH_ATTR 修复 | | v1.9.0 | 37,516 (46%) | **61,987 (94%)** | 408,540 (38%) | 添加了异步库 | | v1.9.1 | 37,644 (46%) | **61,987 (94%)** | 408,844 (38%) | 混合 WiFi 修复 | | v1.9.2 | 37,800 (47%) | **61,987 (94%)** | 409,100 (39%) | WiFi 弹性 | **结论**:内存使用稳定,在 24 小时以上的运行测试中未检测到内存泄漏。 ## 未来计划:Home Assistant 集成 该固件设计为可扩展的。接下来计划的功能: ### 自定义显示屏 通过 REST API 从 Home Assistant 拉取数据: - **智能家居统计**:能源使用情况、房间温度 - **传感器数据**:空气质量,CO2 浓度 - **自动化状态**:警报状态、门锁状态 ### MQTT 集成 - 将时间/天气数据发布到 MQTT 代理 - 订阅主题以显示内容 - 启用自动化触发器(例如,门打开时显示警报) ### WebSocket 实时更新 用 WebSocket 取代轮询,以实现: - 实时配置更改而无需刷新页面 - Web UI 中的实时显示预览 - 固件更新的推送通知 ## 如何刷入此固件 ### 要求 - **硬件**:TJ-56-654 天气时钟或兼容的 ESP-01S + OLED 设置 - **FTDI 适配器**:3.3V USB 转串口(CP2102, FT232RL, CH340) - **软件**:Arduino IDE 1.8.x 或 2.x ### Arduino IDE 设置 1. **安装 ESP8266 开发板支持** - 文件 → 首选项 - 附加开发板管理器网址:`http://arduino.esp8266.com/stable/package_esp8266com_index.json` - 工具 → 开发板 → 开发板管理器 → 搜索 "ESP8266" → 安装 2. **安装所需的库** - 项目 → 包含库 → 管理库 - 安装: - `Adafruit GFX Library` - `Adafruit SSD1306` - `NTPClient` - `WiFiManager` (作者:tzapu) - `AsyncHTTPRequest_Generic` - `ESPAsyncTCP` - `ArduinoJson` (作者:Benoit Blanchon) 3. **开发板配置** - 开发板:“Generic ESP8266 Module” - Flash 大小:“1MB (FS:64KB OTA:~470KB)” - Flash 模式:“DIO” - Flash 频率:“40MHz” - CPU 频率:“80MHz” - 上传速度:“115200” ### 首次刷入(通过 FTDI) 1. **连接 ESP-01S**: FTDI 3.3V → ESP-01S 3V3 FTDI GND → ESP-01S GND FTDI TX → ESP-01S RX FTDI RX → ESP-01S TX FTDI GND → ESP-01S GPIO0 (启动模式) 2. **编译并上传**: - 打开 `weather_clock.ino` - 项目 → 上传 - 等待“上传完成” - 移除 GPIO0 到 GND 的跳线 - 按下复位键或重新上电 3. **初始设置**: - 设备创建 AP:"TJ56654-Setup" - 连接到它(密码:`12345678`) - 捕获门户自动打开 - 选择你的 WiFi 网络并输入密码 - 设备重启并连接 ### 后续更新(OTA) 1. **通过 Web 界面**(最简单): - 浏览至 `http://192.168.x.x/update`(从路由器查找 IP) - 或使用 mDNS:`http://tj56654-clock.local/update` - 登录:`admin` / `admin` - 从 `build/` 文件夹中选择 .bin 文件 - 点击“Update” - 设备自动重启(约 15 秒) 2. **通过 Arduino IDE**: - 工具 → 端口 → 选择 "tj56654-clock at 192.168.x.x" - 项目 → 上传 - 无需任何线缆! ## Web 界面 ### 主页(`/`) 当前时间显示,通过 JavaScript 实时更新(每秒获取 `/api/time`)。 ### 配置页面(`/config`) 全面的设置表单: **WiFi 设置** - SSID - 密码 - 主机名(用于 mDNS) **时间设置** - 时区偏移(与 UTC 相差的秒数) - 启用夏令时(欧洲规则) - NTP 服务器地址 - NTP 同步间隔(秒) - 小时格式(12h/24h) **天气设置** - 启用/禁用切换 - 纬度 - 经度 - 城市名称(用于显示) - 更新间隔(秒) **显示设置** - 亮度(0-7) - 旋转(0°, 90°, 180°, 270°) - 显示轮换间隔(秒) - 显示天气屏幕() - 显示日出/日落屏幕(切换) 所有设置均持久化到 EEPROM,并在重启后保留。 ### 调试页面(`/debug`) 实时诊断信息: - **系统**:运行时间、可用堆内存、芯片 ID、Flash 大小 - **WiFi**:SSID、IP、信号强度、MAC 地址、网关、DNS - **时间**:当前时间、时区、夏令时状态、NTP 同步状态 - **NTP 统计**:上次同步、尝试次数、成功次数、失败次数 - **天气**:温度、日出/日落、上次更新、API 状态 - **网络测试**:互联网连接性、DNS 解析 - **显示**:当前模式、旋转、亮度 非常适合用于排查连接或 API 问题。 ## API 文档 所有端点均返回 JSON(除了用于文件上传的 `/update`)。 ### `GET /api/time` 当前时间信息。 **响应:** ``` { "current": "14:23:45", "date": "2026-01-03", "day": "Friday", "timezone_offset": 0, "dst_active": false } ``` ### `GET /api/status` 系统状态概览。 **响应:** ``` { "wifi": { "ssid": "MyNetwork", "ip": "192.168.1.47", "rssi": -38, "hostname": "tj56654-clock" }, "time": { "current": "14:23:45", "timezone_offset": 0, "ntp_synced": true }, "system": { "uptime": 3627, "free_heap": 35104, "chip_id": "f77134" } } ``` ### `GET /api/weather` 当前天气数据。 **响应:** ``` { "temperature": 15.4, "city": "Portimao", "sunrise": "07:48", "sunset": "17:29", "daylight_hours": 9, "daylight_minutes": 41, "last_update": "14:20:00", "valid": true } ``` ### `GET /api/config` 以 JSON 导出完整配置。 **响应:** ``` { "ssid": "MyNetwork", "timezone_offset": 0, "dst_enabled": true, "brightness": 5, "ntp_server": "pool.ntp.org", "ntp_interval": 3600, "hour_format_24": true, "hostname": "tj56654-clock", "latitude": 37.19, "longitude": -8.54, "city_name": "Portimao", "weather_enabled": true, "weather_interval": 1800, "display_rotation_sec": 5, "show_weather": true, "show_sunrise_sunset": true, "display_orientation": 0 } ``` ### `POST /api/config` 从 JSON 导入配置。 **请求体**:与导出响应结构相同(出于安全考虑,password 字段可选)。 **响应:** ``` { "status": "ok" } ``` 导入后设备会自动重启。 ### `POST /api/eeprom-clear` 恢复出厂设置(清除 EEPROM)。 **响应:** ``` { "status": "cleared" } ``` 设备重启进入 WiFiManager 捕获门户。 ### `POST /api/reboot` 远程重启。 **响应:** ``` { "status": "rebooting" } ``` 设备立即重启。 ## 安全性改进 ### 与原始固件相比的变化 | 问题 | 原始固件 | 自定义固件 | | ---------------------- | ------------------------------ | -------------------------------- | | **WiFi 密码泄露** | 开放 AP 中的明文 | 设置后无开放 AP | | **持久化 AP** | 始终激活 | 仅在首次启动或失败时出现 | | **API 密钥** | QWeather 需要注册 | Open-Meteo(无需密钥) | | **云依赖** | 中国服务器 | 直接 API 调用,无中间商 | | **固件更新** | 仅限手动 FTDI | 通过 WiFi OTA(受密码保护) | | **配置访问** | 无身份验证 | 需要管理员密码 | | **代码透明度** | 闭源 | 开源(你正在阅读它!) | ### 已实施的最佳实践 1. **WiFiManager 超时**:如果未进行配置,AP 将在 180 秒后自动关闭 2. **备用 AP 模式**:如果凭据失败,设备会创建安全的 AP("TJ56654-Clock" 附带密码) 3. **EEPROM 验证**:魔术数检查可防止加载损坏的数据 4. **输入净化**:所有用户输入的缓冲区溢出保护 5. **内存安全**:循环中无动态 String 分配,使用固定大小的缓冲区 6. **错误处理**:优雅降级(例如,即使天气获取失败,显示时间) ### 刷机后的推荐步骤 1. **修改 OTA 密码**:编辑 `.ino` 文件中约第 60 行: ArduinoOTA.setPassword("admin"); // Change this! 2. **修改 Web 管理员密码**:编辑约第 430 行: if (!server.authenticate("admin", "admin")) { // Change this! 3. **设置强 WiFi AP 备用密码**:编辑约第 780 行: WiFi.softAP("TJ56654-Clock", "12345678"); // Change this! 4. **禁用不需要的功能**:如果你不需要天气,在 `/config` 中禁用以节省带宽 ## 经验教训 ### 硬件 1. **务必检查引脚分配**:不要假设标准引脚映射 - 该设备交换了 SDA/SCL 2. **FTDI 是你的朋友**:一个 2 美元的适配器可以解锁任何 ESP8266 设备 3. **透明外壳非常棒**:使调试和识别变得轻而易举 4. **阅读 PCB 丝印**:型号和引脚标签能省去数小时的猜测 ### 软件 1. **异步很难但很值得**:完全非阻塞的架构消除了用户界面的卡顿 2. **IRAM 非常宝贵**:在 ESP8266 上,请大量使用 `ICACHE_FLASH_ATTR` 3. **混合方案行之有效**:不要教条主义 - setup() 中的同步 WiFi 解决了关键的用户体验问题 4. **状态机可扩展**:比复杂的异步操作的回调地狱要好 5. **在真实硬件上测试**:模拟器无法捕获引脚映射错误或内存限制 ### 安全 1. **物联网安全性通常很糟糕**:在信任设备接入网络之前,务必对其进行审计 2. **开源更安全**:闭源固件是一个黑盒 - 你完全不知道它在干什么 3. **默认设置很重要**:不安全的默认设置(开放 AP,明文密码)会导致真实的漏洞 4. **纵深防御**:多层保护(WiFiManager 超时,密码保护,验证)可以捕捉错误 ### 开发 1. **从第一天起就支持 OTA**:通过 FTDI 刷机会很快变得过时 - 尽早构建 OTA 支持 2. **为你的工作版本控制**:备份文件(.bak, .bak2)救了我好几次 3. **随时编写文档**:发布说明和架构文档可防止“我当时在想什么?”的时刻 4. **逐步改进**:v1.7 → v1.8 → v1.9.x 使调试变得可控 ## 致谢 **硬件**:TJ-56-654 天气时钟套件([AliExpress](https://pt.aliexpress.com/item/1005008333782531.html)) **固件**:充满爱与挫败感从头开始编写 **使用的库**: - [ESP8266 Arduino Core](https://github.com/esp8266/Arduino) - [Adafruit SSD1306](https://github.com/adafruit/Adafruit_SSD1306) - [WiFiManager](https://github.com/tzapu/WiFiManager) - [AsyncHTTPRequest_Generic](https://github.com/khoih-prog/AsyncHTTPRequest_Generic) - [NTPClient](https://github.com/arduino-libraries/NTPClient) **APIs**: - [Open-Meteo](https://open-meteo.com/) - 免费的天气 API,无需注册 **工具**: - Arduino IDE 2.x - FTDI FT232RL USB 转串口适配器 - 大量的咖啡 ☕ ## 项目结构 ``` esp8266-weather-clock/ ├── firmware/ │ └── weather_clock/ │ └── weather_clock.ino # Main firmware (~2,100 lines) ├── docs/ │ ├── HARDWARE.md # Hardware specifications │ └── INSTALLATION.md # Flashing guide ├── CHANGELOG.md # Version history └── README.md # This file ``` ## 许可证 本项目已发布到公共领域。你可以用它做任何你想做的事。如果你改进了它,考虑分享你的更改 - 这就是我们让物联网变得更好的方式。 ## 结语 这个项目始于“我不信任这个设备”,终于“我构建了更好的东西”。 原始固件的安全漏洞简直大得能开进一辆卡车。而自定义的替代方案: - ✅ 不会泄露 WiFi 密码 - ✅ 使用免费、开放的 API - ✅ 通过 WiFi 更新 - ✅ 完全异步运行(不卡顿) - ✅ 集成 Home Assistant(即将推出) - ✅ 完全可审计(你正在阅读源码) 总成本:5 欧元的硬件 + 一个周末的修修补补。 如果你有这样一台设备,**请刷入此固件**。如果你正在购买物联网小工具,**务必先审计它们**。如果某物看起来不安全,**自己动手修复它** - 这就是黑客精神。 现在去创造点酷东西吧。🚀 **附言**:如果你觉得这个项目有用,请考虑给仓库点个 Star。如果你发现了错误,请提交一个 Issue。如果你想添加 Home Assistant 屏幕,让我们合作 - 这是我接下来计划的内容! **作者**:apetrochenko **日期**:2026-01-06 **固件版本**:v1.9.4(生产就绪)
标签:AliExpress, CISA项目, DIY套件, DNS 反向解析, ESP8266, Home Assistant, OTA更新, Web界面, WiFi密码泄露, 云资产清单, 固件开发, 天气时钟, 嵌入式系统, 开源固件, 异步编程, 明文传输漏洞, 智能家居, 智能家居集成, 漏洞修复, 物联网安全, 网络安全培训, 网络测绘, 逆向工程, 防御绕过