petrochen/esp8266-weather-clock-opensource
GitHub: petrochen/esp8266-weather-clock-opensource
针对 AliExpress 上存在严重安全漏洞的 ESP8266 天气时钟,逆向工程后重写的安全开源固件,修复了 WiFi 密码明文泄露等问题并增加了 OTA 更新等功能。
Stars: 9 | Forks: 5
# 逆向工程一款 5 欧元的 AliExpress 天气时钟:一个安全故事
## 太长不看(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密码泄露, 云资产清单, 固件开发, 天气时钟, 嵌入式系统, 开源固件, 异步编程, 明文传输漏洞, 智能家居, 智能家居集成, 漏洞修复, 物联网安全, 网络安全培训, 网络测绘, 逆向工程, 防御绕过