This commit is contained in:
MiaoMint
2025-08-12 04:24:15 +08:00
commit 4dcbb9c05b
21 changed files with 3289 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

70
PWM_README.md Normal file
View File

@@ -0,0 +1,70 @@
# PetIO PWM 功能说明
## 新增功能
### PWM 模式支持
- 在手动控制中新增了 PWM 模式开关
- 可以通过滑块调节 PWM 输出强度 (0-100%)
- 在定时器中也支持设置 PWM 模式和强度
### 使用方法
#### 手动控制 PWM
1. 在"手动控制"标签页中
2. 选择要控制的引脚
3. 勾选"PWM 模式"复选框
4. 使用滑块调节 PWM 强度 (0-100%)
5. 设置持续时间0表示切换状态
6. 点击"执行控制"
#### 定时器 PWM
1. 在"定时器管理"标签页中
2. 填写引脚、时间、持续时间等基本信息
3. 勾选"PWM 模式"复选框
4. 使用滑块调节 PWM 强度
5. 设置是否每天重复
6. 点击"添加定时器"
### API 变化
#### 手动控制 API
- 端点: `POST /api/manual`
- 新增参数:
- `isPWM`: 布尔值是否为PWM模式
- `pwmValue`: 整数 (0-1023)PWM强度值
#### 定时器 API
- 端点: `POST /api/timers``PUT /api/timers/{id}`
- 新增参数:
- `isPWM`: 布尔值是否为PWM模式
- `pwmValue`: 整数 (0-1023)PWM强度值
#### PWM 配置 API
- 端点: `GET /api/pwm/config`
- 返回 PWM 配置信息:
- `frequency`: PWM频率
- `resolution`: PWM分辨率
- `maxValue`: PWM最大值
- `minValue`: PWM最小值
- `defaultValue`: PWM默认值
### 技术细节
#### PWM 配置
- 频率: 1 KHz
- 分辨率: 10位 (0-1023)
- ESP8266 的 `analogWrite()` 函数用于PWM输出
#### 数据存储
- PWM 设置会保存到 EEPROM
- 向后兼容旧的定时器配置
#### 显示功能
- 定时器列表会显示PWM模式和强度百分比
- 手动控制页面显示实时PWM强度百分比
### 注意事项
1. 并非所有引脚都支持PWM输出请参考ESP8266文档
2. PWM值范围是0-1023对应0%-100%的输出强度
3. 在数字模式下PWM值被忽略
4. 旧的定时器配置会自动设置为数字模式

267
README.md Normal file
View File

@@ -0,0 +1,267 @@
# PetIO 控制系统
一个基于 ESP8266 的智能引脚定时控制系统,支持 Web 界面管理和 WiFi 配置。
## 功能特性
### 🎯 定时器管理
- ✅ 设置任意引脚在指定时间点开启指定时长
- ✅ 支持多个定时器同时运行
- ✅ 可启用/禁用单个定时器
-**每天重复功能** - 可设置定时器每天自动重复
-**单次执行功能** - 执行一次后自动禁用
- ✅ 定时器状态实时显示
- ✅ 持### 3. 定时器不工作
- 确认设备已连接 WiFiAP 模式下定时器功能受限)
- 检查系统时间是否已同步(状态页面会显示时间来源)
- 确认定时器已启用且设置正确
- 检查引脚连接
### 4. 时间不准确
- 确认设备已连接到互联网
- 检查时区设置是否正确
- 尝试手动触发时间同步(重启设备)配置
-**NTP 时间同步** - WiFi 连接时自动同步网络时间
### 🔌 引脚控制
- ✅ 支持所有 ESP8266 可用引脚 (0,1,2,3,4,5,12,13,14,15,16)
- ✅ 实时显示引脚状态
- ✅ 手动开关控制
- ✅ 定时开启功能
- ✅ 引脚占用状态提示
### 📶 WiFi 管理
- ✅ 自动连接已保存的 WiFi
-**无密码 AP 模式** - 无配置时自动启动无密码热点
- ✅ Web 界面配置 WiFi
- ✅ 支持重置为 AP 模式
- ✅ 连接状态监控和自动重连
-**智能功能限制** - AP 模式下自动隐藏定时器功能
### 🌐 Web 界面
- ✅ 响应式设计,支持手机和电脑
- ✅ 实时数据更新
- ✅ 现代化 UI 设计
- ✅ 多标签页管理
- ✅ 操作反馈和状态提示
-**改进的时间选择器** - 使用 HTML5 time 输入控件
-**智能功能显隐** - 根据连接状态自动调整界面
## 硬件要求
- ESP8266 开发板 (ESP-12E/NodeMCU/Wemos D1 等)
- 连接到引脚的负载设备继电器、LED、水泵等
## 快速开始
### 1. 编译和上传
```bash
# 使用 PlatformIO 编译并上传
pio run --target upload
# 或使用 Arduino IDE
# 选择板子: ESP8266 -> Generic ESP8266 Module
# 上传速度: 921600
# Flash Size: 4MB
```
### 2. 首次配置
1. 上传程序后ESP8266 会自动启动 AP 模式
2. 用手机或电脑连接热点:
- **SSID**: `PetIO_Setup`
- **密码**: 无密码(开放网络)
3. 打开浏览器访问:`http://192.168.4.1`
4. 在 "WiFi 设置" 标签页配置你的 WiFi
5. 配置成功后设备会自动重启并连接到你的 WiFi
### 3. 正常使用
- WiFi 连接成功后,可通过设备的 IP 地址访问控制界面
- 串口监视器会显示设备的访问地址
- **注意**: 只有在 WiFi 连接状态下,定时器功能才能正常工作(需要 NTP 时间同步)
## API 接口文档
### 系统状态
```
GET /api/status
返回: {
"wifiConnected": boolean,
"localIP": "192.168.1.100",
"apIP": "192.168.4.1",
"isAPMode": boolean,
"currentTime": "12:30",
"activeTimers": 2,
"totalTimers": 5
}
```
### 定时器管理
```
# 获取所有定时器
GET /api/timers
# 添加定时器
POST /api/timers
Body: {
"pin": 2,
"hour": 12,
"minute": 30,
"duration": 60,
"repeatDaily": true
}
# 更新定时器
PUT /api/timers/{index}
Body: {
"pin": 2,
"hour": 12,
"minute": 30,
"duration": 60,
"enabled": true,
"repeatDaily": true
}
# 删除定时器
DELETE /api/timers/{index}
# 清除所有定时器
POST /api/timers/clear
```
### 引脚控制
```
# 获取引脚状态
GET /api/pins
# 手动控制
POST /api/manual
Body: {
"pin": 2,
"duration": 0 // 0=切换状态,>0=开启指定秒数
}
```
### WiFi 配置
```
# 保存WiFi配置
POST /api/wifi
Body: {
"ssid": "My_WiFi",
"password": "password123"
}
# 重置为AP模式
POST /api/wifi/reset
```
## 引脚说明
### ESP8266 可用引脚
| 引脚 | 说明 | 注意事项 |
|------|------|----------|
| 0 | GPIO0 | 启动时需要为 HIGH |
| 1 | TX | 串口发送,调试时避免使用 |
| 2 | GPIO2 | 启动时需要为 HIGH有板载 LED |
| 3 | RX | 串口接收,调试时避免使用 |
| 4 | GPIO4 | 安全的通用 IO |
| 5 | GPIO5 | 安全的通用 IO |
| 12 | GPIO12| 安全的通用 IO |
| 13 | GPIO13| 安全的通用 IO |
| 14 | GPIO14| 安全的通用 IO |
| 15 | GPIO15| 启动时需要为 LOW |
| 16 | GPIO16| 无中断功能,适合简单输出 |
### 推荐用法
- **GPIO 4, 5, 12, 13, 14**: 最安全的选择,适合连接继电器等设备
- **GPIO 2**: 适合连接 LED 指示灯
- **GPIO 16**: 适合简单的开关控制
## 项目结构
```
src/
├── main.cpp # 主程序入口
├── config.h # 配置文件和常量定义
├── wifi_manager.h/cpp # WiFi 连接管理
├── timer_manager.h/cpp # 定时器功能管理
├── time_manager.h/cpp # NTP 时间同步管理
├── web_server.h/cpp # Web 服务器和 API
└── web_pages.h # HTML 页面模板
```
## 自定义配置
### 修改默认 AP 信息
`config.h` 中修改:
```cpp
#define DEFAULT_AP_SSID "Your_AP_Name"
#define DEFAULT_AP_PASSWORD "" // 保持为空表示无密码
```
### 调整时间同步设置
```cpp
#define NTP_SERVER "your.ntp.server"
#define TIME_ZONE 8 // 修改为你的时区
#define NTP_UPDATE_INTERVAL 3600000 // 同步间隔(毫秒)
```
### 调整定时器数量
```cpp
#define MAX_TIMERS 20 // 增加到 20 个定时器
```
### 添加新引脚
```cpp
const int AVAILABLE_PINS[] = {0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17};
```
## 故障排除
### 1. 无法连接 WiFi
- 检查 SSID 和密码是否正确
- 确认路由器是 2.4GHz 频段
- 尝试重置为 AP 模式重新配置
### 2. 定时器不工作
- 检查系统时间是否正确(当前为简化版本,基于运行时间)
- 确认定时器已启用
- 检查引脚连接
### 3. 网页无法访问
- 确认设备已连接到正确的网络
- 检查 IP 地址是否正确
- 尝试重启设备
### 4. 引脚无响应
- 检查硬件连接
- 确认引脚编号正确
- 检查负载是否正常
## 开发和扩展
### 添加新功能
项目采用模块化设计,可以轻松添加新功能:
1. **定时功能**: 修改 `timer_manager.cpp`
2. **网页界面**: 修改 `web_pages.h`
3. **API 接口**: 修改 `web_server.cpp`
4. **WiFi 功能**: 修改 `wifi_manager.cpp`
### 调试模式
启用详细日志输出:
```cpp
// 在 Serial.begin() 后添加
Serial.setDebugOutput(true);
```
## 许可证
MIT License - 可自由使用、修改和分发。
## 贡献
欢迎提交 Issue 和 Pull Request
---
**享受你的智能 PetIO 控制系统!** 🐾

37
include/README Normal file
View File

@@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
lib/README Normal file
View File

@@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

24
platformio.ini Normal file
View File

@@ -0,0 +1,24 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp12e]
platform = espressif8266
board = esp12e
framework = arduino
monitor_speed = 115200
lib_deps =
ESP8266WiFi
ESP8266WebServer
ESP8266mDNS
ArduinoJson
EEPROM
NTPClient
Time
olikraus/U8g2

53
src/config.h Normal file
View File

@@ -0,0 +1,53 @@
#ifndef CONFIG_H
#define CONFIG_H
// WiFi 配置
#define DEFAULT_AP_SSID "PetIO_Setup"
#define WIFI_TIMEOUT 30000 // 30秒
// NTP 时间同步配置
#define NTP_SERVER "pool.ntp.org"
#define TIME_ZONE 8 // UTC+8 中国时区
#define NTP_UPDATE_INTERVAL 3600000 // 1小时同步一次
// Web 服务器端口
#define WEB_SERVER_PORT 80
// EEPROM 地址配置
#define EEPROM_SIZE 512
#define WIFI_SSID_ADDR 0
#define WIFI_PASSWORD_ADDR 64
#define TIMER_CONFIG_ADDR 128
// 定时器配置
#define MAX_TIMERS 10
#define MAX_SSID_LENGTH 32
#define MAX_PASSWORD_LENGTH 64
// ESP8266 可用引脚列表
const int AVAILABLE_PINS[] = {0, 1, 2, 3, 12, 13, 14, 15, 16};
const int AVAILABLE_PINS_COUNT = sizeof(AVAILABLE_PINS) / sizeof(AVAILABLE_PINS[0]);
// PWM 配置
#define PWM_FREQUENCY 1000 // PWM 频率 1KHz
#define PWM_RESOLUTION 10 // PWM 分辨率 10位 (0-1023)
#define PWM_MAX_VALUE 1023 // PWM 最大值
// 定时器结构体
struct TimerConfig
{
bool enabled;
int pin;
int hour;
int minute;
int duration; // 持续时间(秒)
bool repeatDaily; // 每天重复
bool isActive; // 当前是否激活
unsigned long startTime; // 开始时间millis时间戳
unsigned long lastTriggerDay; // 上次触发的天数(用于每天重复检查)
bool isPWM; // 是否为PWM模式
int pwmValue; // PWM值 (0-1023)
unsigned long realStartTime; // 真实开始时间(时间戳)
};
#endif

137
src/display_manager.cpp Normal file
View File

@@ -0,0 +1,137 @@
#include "display_manager.h"
#include <Wire.h>
DisplayManager::DisplayManager(WiFiManager *wm, TimerManager *tm, TimeManager *tim)
: wifi(wm), timers(tm), timeM(tim),
u8g2(U8G2_R0, /* reset=*/U8X8_PIN_NONE) {}
bool DisplayManager::begin()
{
Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
u8g2.setI2CAddress(OLED_I2C_ADDRESS << 1); // U8g2 使用 8-bit 地址
u8g2.begin();
u8g2.setContrast(200);
u8g2.enableUTF8Print();
u8g2.setFont(u8g2_font_6x12_tf); // 默认 ASCII 字体,兼容性更好
// 开机画面
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_wqy12_t_chinese1);
u8g2.drawStr(0, 12, "PetIO Booting...");
u8g2.drawStr(0, 28, "OLED init...");
u8g2.sendBuffer();
return true;
}
void DisplayManager::update()
{
unsigned long now = millis();
if (now - lastDraw >= drawInterval)
{
drawScreen();
lastDraw = now;
}
if (now - lastPageSwitch >= pageInterval)
{
pageIndex = (pageIndex + 1) % 3;
lastPageSwitch = now;
}
}
String DisplayManager::uptimeStr()
{
unsigned long ms = millis();
unsigned long s = ms / 1000UL;
unsigned int d = s / 86400UL;
unsigned int h = (s / 3600UL) % 24;
unsigned int m = (s / 60UL) % 60;
unsigned int sec = s % 60;
char buf[32];
if (d > 0)
sprintf(buf, "%ud %02u:%02u:%02u", d, h, m, sec);
else
sprintf(buf, "%02u:%02u:%02u", h, m, sec);
return String(buf);
}
void DisplayManager::drawScreen()
{
u8g2.clearBuffer();
switch (pageIndex)
{
case 0:
drawPageNetwork();
break;
case 1:
drawPageTimers();
break;
case 2:
drawPageUptime();
break;
}
u8g2.sendBuffer();
}
void DisplayManager::drawPageNetwork()
{
// 标题
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(0, 10, "Network");
// SSID 或 AP 名
String line1 = wifi->isConnected() ? String("SSID ") + WiFi.SSID() : String("AP ") + DEFAULT_AP_SSID;
u8g2.drawUTF8(0, 24, line1.c_str());
// IP
String ip = wifi->isConnected() ? wifi->getLocalIP() : wifi->getAPIP();
String ipLine = String("IP ") + ip;
u8g2.drawUTF8(0, 38, ipLine.c_str());
// RSSI/Mode
String line3;
if (wifi->isConnected())
line3 = String("RSSI ") + WiFi.RSSI() + " dBm";
else
line3 = "Mode AP";
u8g2.drawUTF8(0, 52, line3.c_str());
}
void DisplayManager::drawPageTimers()
{
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(0, 10, "Timers");
int total = timers->getTimerCount();
int y = 24;
for (int i = 0; i < total && i < 3; i++)
{ // 最多展示3条
TimerConfig t = timers->getTimer(i);
char buf[32];
snprintf(buf, sizeof(buf), "P%d %02d:%02d %ds %s", t.pin, t.hour, t.minute, t.duration, t.repeatDaily ? "R" : "1");
u8g2.drawStr(0, y, buf);
y += 12;
}
char stat[24];
int active = 0;
for (int i = 0; i < total; i++)
{
if (timers->getTimer(i).isActive)
active++;
}
snprintf(stat, sizeof(stat), "%d active / %d", active, total);
int w = u8g2.getStrWidth(stat);
u8g2.drawStr(128 - w, 64, stat);
}
void DisplayManager::drawPageUptime()
{
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(0, 10, "System");
String timeStr = timeM->getCurrentTimeString();
String dateStr = timeM->getCurrentDateString();
String timeLine = String("Time ") + timeStr;
String dateLine = String("Date ") + dateStr;
String upLine = String("Up ") + uptimeStr();
u8g2.drawUTF8(0, 24, timeLine.c_str());
u8g2.drawUTF8(0, 38, dateLine.c_str());
u8g2.drawUTF8(0, 52, upLine.c_str());
}

47
src/display_manager.h Normal file
View File

@@ -0,0 +1,47 @@
#ifndef DISPLAY_MANAGER_H
#define DISPLAY_MANAGER_H
#include <Arduino.h>
#include <U8g2lib.h>
#include "wifi_manager.h"
#include "timer_manager.h"
#include "time_manager.h"
// 默认 I2C 地址通常为 0x3C若你的模块丝印为 0x3D请在 config.h 中覆盖
#ifndef OLED_I2C_ADDRESS
#define OLED_I2C_ADDRESS 0x3C
#endif
// 默认 I2C 引脚ESP8266: ··)
#ifndef OLED_SDA_PIN
#define OLED_SDA_PIN 4
#endif
#ifndef OLED_SCL_PIN
#define OLED_SCL_PIN 5
#endif
class DisplayManager {
public:
DisplayManager(WiFiManager* wm, TimerManager* tm, TimeManager* tim);
bool begin();
void update();
private:
WiFiManager* wifi;
TimerManager* timers;
TimeManager* timeM;
U8G2_SSD1315_128X64_NONAME_F_HW_I2C u8g2; // 全缓冲,硬件 I2C
unsigned long lastDraw = 0;
const unsigned long drawInterval = 1000; // 1s 刷新
uint8_t pageIndex = 0; // 0:网络 1:任务 2:运行
unsigned long lastPageSwitch = 0;
const unsigned long pageInterval = 3000; // 3s 轮播
void drawScreen();
void drawPageNetwork();
void drawPageTimers();
void drawPageUptime();
String uptimeStr();
};
#endif // DISPLAY_MANAGER_H

131
src/main.cpp Normal file
View File

@@ -0,0 +1,131 @@
#include <Arduino.h>
#include "config.h"
#include "wifi_manager.h"
#include "timer_manager.h"
#include "time_manager.h"
#include "web_server.h"
// #include "display_manager.h"
// 全局对象
WiFiManager wifiManager;
TimerManager timerManager;
TimeManager timeManager;
WebServer webServer(&wifiManager, &timerManager, &timeManager);
// DisplayManager display(&wifiManager, &timerManager, &timeManager);
// 状态变量
unsigned long lastUpdate = 0;
unsigned long lastWiFiCheck = 0;
unsigned long lastTimeUpdate = 0;
const unsigned long UPDATE_INTERVAL = 1000; // 1秒
const unsigned long WIFI_CHECK_INTERVAL = 30000; // 30秒
const unsigned long TIME_UPDATE_INTERVAL = 10000; // 10秒
void setup()
{
Serial.begin(115200);
delay(1000); // 给串口时间稳定
Serial.println();
Serial.println("=================================");
Serial.println("🐾 PetIO 控制系统启动中...");
Serial.println("=================================");
// // 初始化 OLED 显示
// Serial.println("🖥️ 初始化 OLED 显示屏...");
// display.begin();
// Serial.println("✅ OLED 初始化完成!");
// 初始化时间管理器
Serial.println("🕐 初始化时间管理器...");
timeManager.begin();
Serial.println("✅ 时间管理器初始化完成!");
// 初始化定时器管理器
Serial.println("⏰ 初始化定时器管理器...");
timerManager.begin(&timeManager);
Serial.println("✅ 定时器管理器初始化完成!");
// 初始化 WiFi 管理器
Serial.println("📶 初始化 WiFi 管理器...");
bool wifiConnected = wifiManager.begin();
Serial.println("✅ WiFi 管理器初始化完成!");
// 初始化 Web 服务器
Serial.println("🌐 启动 Web 服务器...");
webServer.begin();
Serial.println("✅ Web 服务器启动完成!");
// 启动信息
Serial.println("=================================");
Serial.println("✅ 系统启动完成!");
Serial.println("=================================");
if (wifiConnected)
{
Serial.println("🌍 WiFi 模式 - 可通过以下地址访问:");
Serial.println(" http://" + wifiManager.getLocalIP());
Serial.println("🕐 NTP 时间同步已启用");
}
else
{
Serial.println("📡 AP 模式 - 请连接以下热点:");
Serial.println(" SSID: " + String(DEFAULT_AP_SSID));
Serial.println(" 密码: 无密码");
Serial.println(" 然后访问: http://" + wifiManager.getAPIP());
Serial.println("⚠️ AP 模式下定时器功能受限,请连接 WiFi 以启用完整功能");
}
Serial.println("=================================");
Serial.println();
// 显示可用引脚信息
Serial.print("🔌 可用引脚: ");
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++)
{
Serial.print(AVAILABLE_PINS[i]);
if (i < AVAILABLE_PINS_COUNT - 1)
Serial.print(", ");
}
Serial.println();
// 显示定时器信息
Serial.println("⏰ 已加载定时器数量: " + String(timerManager.getTimerCount()));
Serial.println();
Serial.println("📝 系统日志:");
Serial.println("----------------------------------------");
}
void loop()
{
unsigned long currentTime = millis();
// 处理 Web 请求
webServer.handleClient();
// 定期更新时间同步
if (currentTime - lastTimeUpdate >= TIME_UPDATE_INTERVAL)
{
timeManager.update();
lastTimeUpdate = currentTime;
}
// 定期更新定时器
if (currentTime - lastUpdate >= UPDATE_INTERVAL)
{
timerManager.update();
lastUpdate = currentTime;
}
// 定期检查 WiFi 连接
if (currentTime - lastWiFiCheck >= WIFI_CHECK_INTERVAL)
{
wifiManager.handleWiFiConnection();
lastWiFiCheck = currentTime;
}
// display.update();
// 让系统有时间处理其他任务
yield();
}

135
src/time_manager.cpp Normal file
View File

@@ -0,0 +1,135 @@
#include "time_manager.h"
#include <ESP8266WiFi.h>
TimeManager::TimeManager() : timeClient(ntpUDP, NTP_SERVER, TIME_ZONE * 3600) {
timeInitialized = false;
lastNTPUpdate = 0;
}
void TimeManager::begin() {
timeClient.begin();
timeClient.setUpdateInterval(NTP_UPDATE_INTERVAL);
Serial.println("时间管理器初始化完成");
// 如果 WiFi 已连接,立即尝试同步时间
if (WiFi.status() == WL_CONNECTED) {
forceSync();
}
}
void TimeManager::update() {
// 只有在 WiFi 连接时才更新 NTP 时间
if (WiFi.status() == WL_CONNECTED) {
timeClient.update();
// 检查是否成功获取到时间
if (!timeInitialized && timeClient.getEpochTime() > 0) {
timeInitialized = true;
lastNTPUpdate = millis();
Serial.println("NTP 时间同步成功: " + getCurrentTimeString());
}
// 定期强制同步
if (timeInitialized && millis() - lastNTPUpdate > NTP_UPDATE_INTERVAL) {
forceSync();
}
}
}
bool TimeManager::isTimeValid() {
// WiFi 连接且时间已初始化
return (WiFi.status() == WL_CONNECTED) && timeInitialized && (timeClient.getEpochTime() > 0);
}
int TimeManager::getCurrentHour() {
if (isTimeValid()) {
return timeClient.getHours();
}
// 如果没有有效时间,返回基于运行时间的模拟时间(仅用于测试)
unsigned long seconds = millis() / 1000;
return (seconds / 3600) % 24;
}
int TimeManager::getCurrentMinute() {
if (isTimeValid()) {
return timeClient.getMinutes();
}
// 如果没有有效时间,返回基于运行时间的模拟时间(仅用于测试)
unsigned long seconds = millis() / 1000;
return (seconds / 60) % 60;
}
int TimeManager::getCurrentDay() {
if (isTimeValid()) {
// 返回自 Unix epoch 以来的天数
return timeClient.getEpochTime() / 86400;
}
// 如果没有有效时间,返回基于运行时间的模拟天数
return millis() / (24 * 60 * 60 * 1000UL);
}
String TimeManager::getCurrentTimeString() {
char timeStr[10];
if (isTimeValid()) {
sprintf(timeStr, "%02d:%02d:%02d",
timeClient.getHours(),
timeClient.getMinutes(),
timeClient.getSeconds());
} else {
sprintf(timeStr, "%02d:%02d:%02d*",
getCurrentHour(),
getCurrentMinute(),
(int)((millis() / 1000) % 60));
}
return String(timeStr);
}
String TimeManager::getCurrentDateString() {
if (isTimeValid()) {
unsigned long epochTime = timeClient.getEpochTime();
// 简单的日期计算
int days = epochTime / 86400;
int year = 1970 + (days / 365);
int month = ((days % 365) / 30) + 1;
int day = (days % 30) + 1;
char dateStr[20];
sprintf(dateStr, "%04d-%02d-%02d", year, month, day);
return String(dateStr);
}
return "未同步";
}
void TimeManager::forceSync() {
if (WiFi.status() == WL_CONNECTED) {
Serial.println("强制同步 NTP 时间...");
timeClient.forceUpdate();
if (timeClient.getEpochTime() > 0) {
timeInitialized = true;
lastNTPUpdate = millis();
Serial.println("NTP 同步成功: " + getCurrentTimeString());
} else {
Serial.println("NTP 同步失败");
}
}
}
bool TimeManager::isWiFiTimeAvailable() {
return WiFi.status() == WL_CONNECTED;
}
unsigned long TimeManager::getEpochTime() {
if (isTimeValid()) {
return timeClient.getEpochTime();
}
return 0; // 无效时间返回0
}

31
src/time_manager.h Normal file
View File

@@ -0,0 +1,31 @@
#ifndef TIME_MANAGER_H
#define TIME_MANAGER_H
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <TimeLib.h>
#include "config.h"
class TimeManager {
private:
WiFiUDP ntpUDP;
NTPClient timeClient;
bool timeInitialized;
unsigned long lastNTPUpdate;
public:
TimeManager();
void begin();
void update();
bool isTimeValid();
int getCurrentHour();
int getCurrentMinute();
int getCurrentDay(); // 获取当前日期(用于每天重复检查)
unsigned long getEpochTime(); // 获取当前时间戳
String getCurrentTimeString();
String getCurrentDateString();
void forceSync();
bool isWiFiTimeAvailable();
};
#endif

560
src/timer_manager.cpp Normal file
View File

@@ -0,0 +1,560 @@
#include "timer_manager.h"
TimerManager::TimerManager() {
timerCount = 0;
timeManager = nullptr;
lastStateSave = 0;
}
void TimerManager::begin(TimeManager* tm) {
timeManager = tm;
// 初始化EEPROM
EEPROM.begin(EEPROM_SIZE);
// 初始化所有可用引脚为输出模式
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) {
pinMode(AVAILABLE_PINS[i], OUTPUT);
digitalWrite(AVAILABLE_PINS[i], LOW);
// 为PWM引脚设置频率
analogWriteFreq(PWM_FREQUENCY);
analogWriteResolution(PWM_RESOLUTION);
}
loadTimers();
Serial.println("Timer Manager 初始化完成,已加载 " + String(timerCount) + " 个定时器");
// 输出恢复的状态信息
int activeCount = 0;
for (int i = 0; i < timerCount; i++) {
if (timers[i].isActive) {
activeCount++;
}
}
if (activeCount > 0) {
Serial.println("已恢复 " + String(activeCount) + " 个活跃定时器状态");
}
}
void TimerManager::update() {
if (!timeManager) return;
unsigned long currentTime = millis();
int currentHour = timeManager->getCurrentHour();
int currentMinute = timeManager->getCurrentMinute();
unsigned long currentDay = timeManager->getCurrentDay();
bool stateChanged = false;
for (int i = 0; i < timerCount; i++) {
if (!timers[i].enabled) continue;
// 检查是否到达触发时间
bool shouldTrigger = false;
if (!timers[i].isActive &&
timers[i].hour == currentHour &&
timers[i].minute == currentMinute) {
if (timers[i].repeatDaily) {
// 每天重复:检查今天是否已经触发过
if (timers[i].lastTriggerDay != currentDay) {
shouldTrigger = true;
timers[i].lastTriggerDay = currentDay;
}
} else {
// 单次触发
shouldTrigger = true;
}
}
if (shouldTrigger) {
timers[i].isActive = true;
timers[i].startTime = currentTime;
// 保存真实时间戳(如果可用)
if (timeManager->isTimeValid()) {
timers[i].realStartTime = timeManager->getEpochTime();
} else {
timers[i].realStartTime = 0;
}
setPin(timers[i].pin, HIGH, timers[i].isPWM ? timers[i].pwmValue : 0);
String modeStr = timers[i].isPWM ? " PWM(" + String(timers[i].pwmValue) + ")" : "";
Serial.println("定时器 " + String(i) + " 激活,引脚 " + String(timers[i].pin) + " 开启" + modeStr +
(timers[i].repeatDaily ? " (每天重复)" : " (单次)"));
stateChanged = true;
// 如果是单次定时器,触发后自动禁用
if (!timers[i].repeatDaily) {
timers[i].enabled = false;
saveTimers(); // 立即保存配置更改
}
}
// 检查是否需要关闭
if (timers[i].isActive &&
currentTime - timers[i].startTime >= (unsigned long)(timers[i].duration * 1000)) {
timers[i].isActive = false;
timers[i].realStartTime = 0; // 清理真实时间戳
setPin(timers[i].pin, LOW, 0);
Serial.println("定时器 " + String(i) + " 完成,引脚 " + String(timers[i].pin) + " 关闭");
stateChanged = true;
}
}
// 定期保存状态,或者有状态变化时立即保存
if (stateChanged || (currentTime - lastStateSave >= STATE_SAVE_INTERVAL)) {
saveTimerStates();
lastStateSave = currentTime;
}
}
bool TimerManager::addTimer(int pin, int hour, int minute, int duration, bool repeatDaily, bool isPWM, int pwmValue) {
if (timerCount >= MAX_TIMERS) {
return false;
}
// 验证引脚是否可用
bool pinValid = false;
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) {
if (AVAILABLE_PINS[i] == pin) {
pinValid = true;
break;
}
}
if (!pinValid) return false;
// 验证时间
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || duration <= 0) {
return false;
}
// 验证PWM值
if (isPWM && (pwmValue < 0 || pwmValue > PWM_MAX_VALUE)) {
return false;
}
timers[timerCount].enabled = true;
timers[timerCount].pin = pin;
timers[timerCount].hour = hour;
timers[timerCount].minute = minute;
timers[timerCount].duration = duration;
timers[timerCount].repeatDaily = repeatDaily;
timers[timerCount].isActive = false;
timers[timerCount].startTime = 0;
timers[timerCount].lastTriggerDay = 0; // 初始化为0
timers[timerCount].isPWM = isPWM;
timers[timerCount].pwmValue = isPWM ? pwmValue : 0;
timers[timerCount].realStartTime = 0; // 初始化真实时间戳
timerCount++;
saveTimers();
String modeStr = isPWM ? " PWM模式, 值=" + String(pwmValue) : " 数字模式";
Serial.println("添加定时器:引脚 " + String(pin) + ", 时间 " + String(hour) + ":" + String(minute) +
", 持续 " + String(duration) + "" + modeStr + (repeatDaily ? " (每天重复)" : " (单次)"));
return true;
}
bool TimerManager::removeTimer(int index) {
if (index < 0 || index >= timerCount) {
return false;
}
// 如果定时器正在运行,先关闭引脚
if (timers[index].isActive) {
setPin(timers[index].pin, LOW, 0);
}
// 移动数组元素
for (int i = index; i < timerCount - 1; i++) {
timers[i] = timers[i + 1];
}
timerCount--;
saveTimers();
Serial.println("删除定时器 " + String(index));
return true;
}
bool TimerManager::updateTimer(int index, int pin, int hour, int minute, int duration, bool enabled, bool repeatDaily, bool isPWM, int pwmValue) {
if (index < 0 || index >= timerCount) {
return false;
}
// 验证引脚
bool pinValid = false;
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) {
if (AVAILABLE_PINS[i] == pin) {
pinValid = true;
break;
}
}
if (!pinValid) return false;
// 验证时间
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || duration <= 0) {
return false;
}
// 验证PWM值
if (isPWM && (pwmValue < 0 || pwmValue > PWM_MAX_VALUE)) {
return false;
}
// 如果定时器正在运行且引脚发生变化,先关闭旧引脚
if (timers[index].isActive && timers[index].pin != pin) {
setPin(timers[index].pin, LOW, 0);
timers[index].isActive = false;
}
timers[index].enabled = enabled;
timers[index].pin = pin;
timers[index].hour = hour;
timers[index].minute = minute;
timers[index].duration = duration;
timers[index].repeatDaily = repeatDaily;
timers[index].isPWM = isPWM;
timers[index].pwmValue = isPWM ? pwmValue : 0;
// 如果修改了重复设置,重置触发状态
timers[index].lastTriggerDay = 0;
saveTimers();
Serial.println("更新定时器 " + String(index));
return true;
}
String TimerManager::getTimersJSON() {
DynamicJsonDocument doc(2048);
JsonArray array = doc.to<JsonArray>();
for (int i = 0; i < timerCount; i++) {
JsonObject timer = array.createNestedObject();
timer["index"] = i;
timer["enabled"] = timers[i].enabled;
timer["pin"] = timers[i].pin;
timer["hour"] = timers[i].hour;
timer["minute"] = timers[i].minute;
timer["duration"] = timers[i].duration;
timer["repeatDaily"] = timers[i].repeatDaily;
timer["isActive"] = timers[i].isActive;
timer["isPWM"] = timers[i].isPWM;
timer["pwmValue"] = timers[i].pwmValue;
}
String result;
serializeJson(doc, result);
return result;
}
String TimerManager::getAvailablePinsJSON() {
DynamicJsonDocument doc(1024);
JsonArray array = doc.to<JsonArray>();
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) {
JsonObject pin = array.createNestedObject();
pin["pin"] = AVAILABLE_PINS[i];
pin["state"] = digitalRead(AVAILABLE_PINS[i]);
// 检查是否被定时器占用
bool inUse = false;
for (int j = 0; j < timerCount; j++) {
if (timers[j].pin == AVAILABLE_PINS[i] && timers[j].isActive) {
inUse = true;
break;
}
}
pin["inUse"] = inUse;
}
String result;
serializeJson(doc, result);
return result;
}
void TimerManager::executeManualControl(int pin, int duration, bool isPWM, int pwmValue) {
// 验证引脚
bool pinValid = false;
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) {
if (AVAILABLE_PINS[i] == pin) {
pinValid = true;
break;
}
}
if (!pinValid) return;
// 验证PWM值
if (isPWM && (pwmValue < 0 || pwmValue > PWM_MAX_VALUE)) {
return;
}
if (duration > 0) {
// 开启引脚指定时间
setPin(pin, HIGH, isPWM ? pwmValue : 0);
String modeStr = isPWM ? " PWM模式, 值=" + String(pwmValue) : " 数字模式";
Serial.println("手动控制:引脚 " + String(pin) + " 开启 " + String(duration) + "" + modeStr);
// 这里应该使用定时器来关闭,简化处理
delay(duration * 1000);
setPin(pin, LOW, 0);
Serial.println("手动控制:引脚 " + String(pin) + " 关闭");
} else {
// 切换状态
bool currentState = digitalRead(pin);
if (isPWM) {
// PWM模式切换
if (currentState) {
setPin(pin, LOW, 0);
Serial.println("手动控制:引脚 " + String(pin) + " PWM关闭");
} else {
setPin(pin, HIGH, pwmValue);
Serial.println("手动控制:引脚 " + String(pin) + " PWM开启, 值=" + String(pwmValue));
}
} else {
// 数字模式切换
setPin(pin, !currentState, 0);
Serial.println("手动控制:引脚 " + String(pin) + " 数字切换到 " + String(!currentState));
}
}
}
void TimerManager::saveTimers() {
int addr = TIMER_CONFIG_ADDR;
// 保存定时器数量
EEPROM.write(addr++, timerCount);
// 保存每个定时器
for (int i = 0; i < timerCount; i++) {
EEPROM.write(addr++, timers[i].enabled ? 1 : 0);
EEPROM.write(addr++, timers[i].pin);
EEPROM.write(addr++, timers[i].hour);
EEPROM.write(addr++, timers[i].minute);
EEPROM.write(addr++, (timers[i].duration >> 8) & 0xFF);
EEPROM.write(addr++, timers[i].duration & 0xFF);
EEPROM.write(addr++, timers[i].repeatDaily ? 1 : 0);
EEPROM.write(addr++, timers[i].isPWM ? 1 : 0);
EEPROM.write(addr++, (timers[i].pwmValue >> 8) & 0xFF);
EEPROM.write(addr++, timers[i].pwmValue & 0xFF);
// 保存运行时状态
EEPROM.write(addr++, timers[i].isActive ? 1 : 0);
// 保存开始时间4字节
unsigned long startTime = timers[i].startTime;
EEPROM.write(addr++, (startTime >> 24) & 0xFF);
EEPROM.write(addr++, (startTime >> 16) & 0xFF);
EEPROM.write(addr++, (startTime >> 8) & 0xFF);
EEPROM.write(addr++, startTime & 0xFF);
// 保存上次触发天数4字节
unsigned long lastTriggerDay = timers[i].lastTriggerDay;
EEPROM.write(addr++, (lastTriggerDay >> 24) & 0xFF);
EEPROM.write(addr++, (lastTriggerDay >> 16) & 0xFF);
EEPROM.write(addr++, (lastTriggerDay >> 8) & 0xFF);
EEPROM.write(addr++, lastTriggerDay & 0xFF);
// 保存真实开始时间4字节
unsigned long realStartTime = timers[i].realStartTime;
EEPROM.write(addr++, (realStartTime >> 24) & 0xFF);
EEPROM.write(addr++, (realStartTime >> 16) & 0xFF);
EEPROM.write(addr++, (realStartTime >> 8) & 0xFF);
EEPROM.write(addr++, realStartTime & 0xFF);
}
EEPROM.commit();
}
void TimerManager::loadTimers() {
int addr = TIMER_CONFIG_ADDR;
// 读取定时器数量
timerCount = EEPROM.read(addr++);
if (timerCount > MAX_TIMERS) timerCount = 0;
// 读取每个定时器
for (int i = 0; i < timerCount; i++) {
timers[i].enabled = EEPROM.read(addr++) == 1;
timers[i].pin = EEPROM.read(addr++);
timers[i].hour = EEPROM.read(addr++);
timers[i].minute = EEPROM.read(addr++);
int durationHigh = EEPROM.read(addr++);
int durationLow = EEPROM.read(addr++);
timers[i].duration = (durationHigh << 8) | durationLow;
timers[i].repeatDaily = EEPROM.read(addr++) == 1;
// 检查是否有PWM数据向后兼容
if (addr < EEPROM_SIZE - 16) { // 增加了13个字节的运行时状态数据9+4
timers[i].isPWM = EEPROM.read(addr++) == 1;
int pwmHigh = EEPROM.read(addr++);
int pwmLow = EEPROM.read(addr++);
timers[i].pwmValue = (pwmHigh << 8) | pwmLow;
// 读取运行时状态
timers[i].isActive = EEPROM.read(addr++) == 1;
// 读取开始时间4字节
unsigned long startTime = 0;
startTime |= ((unsigned long)EEPROM.read(addr++)) << 24;
startTime |= ((unsigned long)EEPROM.read(addr++)) << 16;
startTime |= ((unsigned long)EEPROM.read(addr++)) << 8;
startTime |= (unsigned long)EEPROM.read(addr++);
timers[i].startTime = startTime;
// 读取上次触发天数4字节
unsigned long lastTriggerDay = 0;
lastTriggerDay |= ((unsigned long)EEPROM.read(addr++)) << 24;
lastTriggerDay |= ((unsigned long)EEPROM.read(addr++)) << 16;
lastTriggerDay |= ((unsigned long)EEPROM.read(addr++)) << 8;
lastTriggerDay |= (unsigned long)EEPROM.read(addr++);
timers[i].lastTriggerDay = lastTriggerDay;
// 读取真实开始时间4字节
unsigned long realStartTime = 0;
realStartTime |= ((unsigned long)EEPROM.read(addr++)) << 24;
realStartTime |= ((unsigned long)EEPROM.read(addr++)) << 16;
realStartTime |= ((unsigned long)EEPROM.read(addr++)) << 8;
realStartTime |= (unsigned long)EEPROM.read(addr++);
timers[i].realStartTime = realStartTime;
} else {
// 旧数据没有PWM和运行时状态信息设为默认值
timers[i].isPWM = false;
timers[i].pwmValue = 0;
timers[i].isActive = false;
timers[i].startTime = 0;
timers[i].lastTriggerDay = 0;
timers[i].realStartTime = 0;
}
// 恢复活跃定时器的引脚状态
if (timers[i].isActive) {
// 使用真实时间检查定时器是否应该仍然活跃
if (timeManager && timeManager->isTimeValid() && timers[i].realStartTime > 0) {
// 需要添加获取当前时间戳的方法到TimeManager
// 暂时使用millis()逻辑,但添加更好的时间处理
unsigned long currentTime = millis();
// 重置开始时间为当前时间减去已经运行的时间
// 这样可以在重启后继续正确计时
if (timers[i].startTime > currentTime) {
// millis() 已重置,重新计算开始时间
timers[i].startTime = currentTime;
Serial.println("重启后调整定时器 " + String(i) + " 开始时间");
}
unsigned long elapsedTime = currentTime - timers[i].startTime;
// 检查定时器是否已经超时
if (elapsedTime >= (unsigned long)(timers[i].duration * 1000)) {
// 定时器已经超时,关闭它
timers[i].isActive = false;
timers[i].realStartTime = 0;
setPin(timers[i].pin, LOW, 0);
Serial.println("重启后发现定时器 " + String(i) + " 已超时,关闭引脚 " + String(timers[i].pin));
} else {
// 定时器仍然有效,恢复引脚状态
setPin(timers[i].pin, HIGH, timers[i].isPWM ? timers[i].pwmValue : 0);
String modeStr = timers[i].isPWM ? " PWM(" + String(timers[i].pwmValue) + ")" : "";
unsigned long remainingTime = (timers[i].duration * 1000) - elapsedTime;
Serial.println("恢复定时器 " + String(i) + " 状态,引脚 " + String(timers[i].pin) + " 开启" + modeStr +
",剩余时间: " + String(remainingTime / 1000) + "");
}
} else {
// 没有有效的时间或者是旧格式数据,保守处理
unsigned long currentTime = millis();
timers[i].startTime = currentTime;
setPin(timers[i].pin, HIGH, timers[i].isPWM ? timers[i].pwmValue : 0);
String modeStr = timers[i].isPWM ? " PWM(" + String(timers[i].pwmValue) + ")" : "";
Serial.println("恢复定时器 " + String(i) + " 状态,引脚 " + String(timers[i].pin) + " 开启" + modeStr +
"(重启后重新计时)");
}
}
}
}
bool TimerManager::hasValidTime() {
return timeManager && timeManager->isTimeValid();
}
void TimerManager::saveTimerStates() {
// 只保存运行时状态,不保存完整配置
// 这样可以减少EEPROM写入次数延长寿命
int addr = TIMER_CONFIG_ADDR + 1; // 跳过定时器数量
for (int i = 0; i < timerCount; i++) {
// 跳过基本配置部分 (10字节)
addr += 10;
// 更新运行时状态部分 (13字节)
EEPROM.write(addr++, timers[i].isActive ? 1 : 0);
// 更新开始时间4字节
unsigned long startTime = timers[i].startTime;
EEPROM.write(addr++, (startTime >> 24) & 0xFF);
EEPROM.write(addr++, (startTime >> 16) & 0xFF);
EEPROM.write(addr++, (startTime >> 8) & 0xFF);
EEPROM.write(addr++, startTime & 0xFF);
// 更新上次触发天数4字节
unsigned long lastTriggerDay = timers[i].lastTriggerDay;
EEPROM.write(addr++, (lastTriggerDay >> 24) & 0xFF);
EEPROM.write(addr++, (lastTriggerDay >> 16) & 0xFF);
EEPROM.write(addr++, (lastTriggerDay >> 8) & 0xFF);
EEPROM.write(addr++, lastTriggerDay & 0xFF);
// 更新真实开始时间4字节
unsigned long realStartTime = timers[i].realStartTime;
EEPROM.write(addr++, (realStartTime >> 24) & 0xFF);
EEPROM.write(addr++, (realStartTime >> 16) & 0xFF);
EEPROM.write(addr++, (realStartTime >> 8) & 0xFF);
EEPROM.write(addr++, realStartTime & 0xFF);
}
EEPROM.commit();
}
void TimerManager::clearAllTimers() {
// 关闭所有激活的引脚
for (int i = 0; i < timerCount; i++) {
if (timers[i].isActive) {
setPin(timers[i].pin, LOW, 0);
}
}
timerCount = 0;
saveTimers();
Serial.println("所有定时器已清除");
}
int TimerManager::getTimerCount() {
return timerCount;
}
TimerConfig TimerManager::getTimer(int index) {
if (index >= 0 && index < timerCount) {
return timers[index];
}
TimerConfig empty = {false, 0, 0, 0, 0, false, false, 0, 0UL, false, 0, 0UL};
return empty;
}
void TimerManager::setPin(int pin, bool state, int pwmValue) {
if (pwmValue > 0 && state) {
// PWM 模式
analogWrite(pin, pwmValue);
} else {
// 数字模式
digitalWrite(pin, state ? HIGH : LOW);
}
}

38
src/timer_manager.h Normal file
View File

@@ -0,0 +1,38 @@
#ifndef TIMER_MANAGER_H
#define TIMER_MANAGER_H
#include <Arduino.h>
#include <EEPROM.h>
#include <ArduinoJson.h>
#include "config.h"
#include "time_manager.h"
class TimerManager {
private:
TimerConfig timers[MAX_TIMERS];
int timerCount;
TimeManager* timeManager;
unsigned long lastStateSave;
const unsigned long STATE_SAVE_INTERVAL = 30000; // 30秒保存一次状态
public:
TimerManager();
void begin(TimeManager* tm);
void update();
bool addTimer(int pin, int hour, int minute, int duration, bool repeatDaily = false, bool isPWM = false, int pwmValue = 512);
bool removeTimer(int index);
bool updateTimer(int index, int pin, int hour, int minute, int duration, bool enabled, bool repeatDaily = false, bool isPWM = false, int pwmValue = 512);
String getTimersJSON();
void saveTimers();
void saveTimerStates(); // 新增:仅保存运行时状态
void loadTimers();
void clearAllTimers();
int getTimerCount();
TimerConfig getTimer(int index);
void setPin(int pin, bool state, int pwmValue = 0);
String getAvailablePinsJSON();
void executeManualControl(int pin, int duration, bool isPWM = false, int pwmValue = 512);
bool hasValidTime();
};
#endif

1050
src/web_pages.h Normal file

File diff suppressed because it is too large Load Diff

422
src/web_server.cpp Normal file
View File

@@ -0,0 +1,422 @@
#include "web_server.h"
WebServer::WebServer(WiFiManager* wm, TimerManager* tm, TimeManager* timeM) : server(WEB_SERVER_PORT) {
wifiManager = wm;
timerManager = tm;
timeManager = timeM;
}
void WebServer::begin() {
setupRoutes();
server.begin();
Serial.println("Web 服务器启动,端口: " + String(WEB_SERVER_PORT));
}
void WebServer::handleClient() {
server.handleClient();
}
void WebServer::setupRoutes() {
// 主页
server.on("/", HTTP_GET, [this]() { handleRoot(); });
// API 路由
server.on("/api/status", HTTP_GET, [this]() { handleGetStatus(); });
server.on("/api/system", HTTP_GET, [this]() { handleGetSystemInfo(); });
server.on("/api/timers", HTTP_GET, [this]() { handleGetTimers(); });
server.on("/api/timers", HTTP_POST, [this]() { handleAddTimer(); });
server.on("/api/timers/clear", HTTP_POST, [this]() { handleClearTimers(); });
server.on("/api/pins", HTTP_GET, [this]() { handleGetPins(); });
server.on("/api/pwm/config", HTTP_GET, [this]() { handleGetPWMConfig(); });
server.on("/api/manual", HTTP_POST, [this]() { handleManualControl(); });
server.on("/api/wifi", HTTP_POST, [this]() { handleWiFiConfig(); });
server.on("/api/wifi/reset", HTTP_POST, [this]() { handleWiFiReset(); });
server.on("/api/restart-ap", HTTP_POST, [this]() { handleRestartAP(); });
// 404 处理 - 这里会处理动态路由
server.onNotFound([this]() {
String uri = server.uri();
// 处理定时器的 PUT 和 DELETE 请求
if (uri.startsWith("/api/timers/") && uri != "/api/timers/clear") {
if (server.method() == HTTP_PUT) {
handleUpdateTimer();
return;
} else if (server.method() == HTTP_DELETE) {
handleDeleteTimer();
return;
}
}
// 真正的 404
server.send(404, "text/plain", "Not Found");
});
}
void WebServer::handleRoot() {
enableCORS();
// 如果设备处于AP模式且未连接WiFi显示简化的WiFi设置页面
if (wifiManager->isInAPMode() && !wifiManager->isConnected()) {
server.send_P(200, "text/html", AP_MODE_HTML);
} else {
// 否则显示完整的控制面板
server.send_P(200, "text/html", INDEX_HTML);
}
}
void WebServer::handleGetStatus() {
enableCORS();
DynamicJsonDocument doc(1024);
doc["wifiConnected"] = wifiManager->isConnected();
doc["localIP"] = wifiManager->getLocalIP();
doc["apIP"] = wifiManager->getAPIP();
doc["isAPMode"] = wifiManager->isInAPMode();
doc["hasValidTime"] = timerManager->hasValidTime();
doc["timeSource"] = timerManager->hasValidTime() ? "NTP" : "系统运行时间";
// 获取当前时间
doc["currentTime"] = timeManager->getCurrentTimeString();
doc["currentDate"] = timeManager->getCurrentDateString();
// 统计活跃定时器
int activeCount = 0;
for (int i = 0; i < timerManager->getTimerCount(); i++) {
TimerConfig timer = timerManager->getTimer(i);
if (timer.isActive) activeCount++;
}
doc["activeTimers"] = activeCount;
doc["totalTimers"] = timerManager->getTimerCount();
sendJSON(doc);
}
void WebServer::handleGetSystemInfo() {
enableCORS();
DynamicJsonDocument doc(2048);
// 基本系统信息
doc["chipId"] = String(ESP.getChipId(), HEX);
doc["flashChipId"] = String(ESP.getFlashChipId(), HEX);
doc["flashChipSize"] = ESP.getFlashChipSize();
doc["flashChipRealSize"] = ESP.getFlashChipRealSize();
doc["flashChipSpeed"] = ESP.getFlashChipSpeed();
doc["cpuFreqMHz"] = ESP.getCpuFreqMHz();
doc["freeHeap"] = ESP.getFreeHeap();
doc["heapFragmentation"] = ESP.getHeapFragmentation();
doc["maxFreeBlockSize"] = ESP.getMaxFreeBlockSize();
doc["freeSketchSpace"] = ESP.getFreeSketchSpace();
doc["sketchSize"] = ESP.getSketchSize();
doc["sketchMD5"] = ESP.getSketchMD5();
doc["resetReason"] = ESP.getResetReason();
doc["resetInfo"] = ESP.getResetInfo();
// 运行时间信息
unsigned long uptime = millis();
doc["uptimeMs"] = uptime;
doc["uptimeDays"] = uptime / (24 * 60 * 60 * 1000UL);
doc["uptimeHours"] = (uptime / (60 * 60 * 1000UL)) % 24;
doc["uptimeMinutes"] = (uptime / (60 * 1000UL)) % 60;
doc["uptimeSeconds"] = (uptime / 1000UL) % 60;
// WiFi 详细信息
JsonObject wifi = doc.createNestedObject("wifi");
wifi["status"] = WiFi.status();
wifi["statusText"] = wifiManager->isConnected() ? "已连接" : (wifiManager->isInAPMode() ? "AP模式" : "断开连接");
wifi["mode"] = WiFi.getMode();
wifi["modeText"] = WiFi.getMode() == WIFI_STA ? "STA" : (WiFi.getMode() == WIFI_AP ? "AP" : "AP+STA");
if (wifiManager->isConnected()) {
wifi["ssid"] = WiFi.SSID();
wifi["bssid"] = WiFi.BSSIDstr();
wifi["rssi"] = WiFi.RSSI();
wifi["localIP"] = WiFi.localIP().toString();
wifi["subnetMask"] = WiFi.subnetMask().toString();
wifi["gatewayIP"] = WiFi.gatewayIP().toString();
wifi["dnsIP"] = WiFi.dnsIP().toString();
wifi["macAddress"] = WiFi.macAddress();
wifi["channel"] = WiFi.channel();
wifi["autoConnect"] = WiFi.getAutoConnect();
}
if (wifiManager->isInAPMode()) {
wifi["apSSID"] = WiFi.softAPSSID();
wifi["apIP"] = WiFi.softAPIP().toString();
wifi["apMacAddress"] = WiFi.softAPmacAddress();
wifi["apStationCount"] = WiFi.softAPgetStationNum();
}
// 时间信息
JsonObject timeInfo = doc.createNestedObject("time");
timeInfo["hasValidTime"] = timeManager->isTimeValid();
timeInfo["timeSource"] = timeManager->isTimeValid() ? "NTP" : "系统运行时间";
timeInfo["currentTime"] = timeManager->getCurrentTimeString();
timeInfo["currentDate"] = timeManager->getCurrentDateString();
timeInfo["isWiFiTimeAvailable"] = timeManager->isWiFiTimeAvailable();
// 引脚状态
JsonArray pins = doc.createNestedArray("pins");
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) {
JsonObject pin = pins.createNestedObject();
pin["pin"] = AVAILABLE_PINS[i];
pin["state"] = digitalRead(AVAILABLE_PINS[i]);
// 检查是否被定时器占用
bool inUse = false;
int timerIndex = -1;
for (int j = 0; j < timerManager->getTimerCount(); j++) {
TimerConfig timer = timerManager->getTimer(j);
if (timer.pin == AVAILABLE_PINS[i] && timer.isActive) {
inUse = true;
timerIndex = j;
break;
}
}
pin["inUse"] = inUse;
pin["timerIndex"] = timerIndex;
}
// 定时器统计
JsonObject timerStats = doc.createNestedObject("timerStats");
timerStats["total"] = timerManager->getTimerCount();
int enabled = 0, active = 0, repeatDaily = 0, oneTime = 0;
for (int i = 0; i < timerManager->getTimerCount(); i++) {
TimerConfig timer = timerManager->getTimer(i);
if (timer.enabled) enabled++;
if (timer.isActive) active++;
if (timer.repeatDaily) repeatDaily++;
else oneTime++;
}
timerStats["enabled"] = enabled;
timerStats["active"] = active;
timerStats["repeatDaily"] = repeatDaily;
timerStats["oneTime"] = oneTime;
timerStats["maxTimers"] = MAX_TIMERS;
// EEPROM 使用情况
JsonObject eeprom = doc.createNestedObject("eeprom");
eeprom["size"] = EEPROM_SIZE;
eeprom["wifiConfigStart"] = WIFI_SSID_ADDR;
eeprom["wifiConfigSize"] = TIMER_CONFIG_ADDR - WIFI_SSID_ADDR;
eeprom["timerConfigStart"] = TIMER_CONFIG_ADDR;
eeprom["timerConfigUsed"] = 1 + (timerManager->getTimerCount() * 7); // 1 byte count + 7 bytes per timer
sendJSON(doc);
}
void WebServer::handleGetTimers() {
enableCORS();
server.send(200, "application/json", timerManager->getTimersJSON());
}
void WebServer::handleAddTimer() {
enableCORS();
if (!server.hasArg("plain")) {
sendJSON(400, "缺少请求数据", false);
return;
}
DynamicJsonDocument doc(1024);
deserializeJson(doc, server.arg("plain"));
int pin = doc["pin"];
int hour = doc["hour"];
int minute = doc["minute"];
int duration = doc["duration"];
bool repeatDaily = doc["repeatDaily"].as<bool>();
bool isPWM = doc["isPWM"].as<bool>();
int pwmValue = doc["pwmValue"].is<int>() ? doc["pwmValue"].as<int>() : 512; // 默认值50%
if (timerManager->addTimer(pin, hour, minute, duration, repeatDaily, isPWM, pwmValue)) {
sendJSON(200, "定时器添加成功");
} else {
sendJSON(400, "定时器添加失败,请检查参数", false);
}
}
void WebServer::handleUpdateTimer() {
enableCORS();
String uri = server.uri();
int lastSlash = uri.lastIndexOf('/');
if (lastSlash == -1) {
sendJSON(400, "无效的请求路径", false);
return;
}
int index = uri.substring(lastSlash + 1).toInt();
if (!server.hasArg("plain")) {
sendJSON(400, "缺少请求数据", false);
return;
}
DynamicJsonDocument doc(1024);
deserializeJson(doc, server.arg("plain"));
int pin = doc["pin"];
int hour = doc["hour"];
int minute = doc["minute"];
int duration = doc["duration"];
bool enabled = doc["enabled"];
bool repeatDaily = doc["repeatDaily"].as<bool>();
bool isPWM = doc["isPWM"].as<bool>();
int pwmValue = doc["pwmValue"].is<int>() ? doc["pwmValue"].as<int>() : 512; // 默认值50%
if (timerManager->updateTimer(index, pin, hour, minute, duration, enabled, repeatDaily, isPWM, pwmValue)) {
sendJSON(200, "定时器更新成功");
} else {
sendJSON(400, "定时器更新失败", false);
}
}
void WebServer::handleDeleteTimer() {
enableCORS();
String uri = server.uri();
int lastSlash = uri.lastIndexOf('/');
if (lastSlash == -1) {
sendJSON(400, "无效的请求路径", false);
return;
}
int index = uri.substring(lastSlash + 1).toInt();
if (timerManager->removeTimer(index)) {
sendJSON(200, "定时器删除成功");
} else {
sendJSON(400, "定时器删除失败", false);
}
}
void WebServer::handleClearTimers() {
enableCORS();
timerManager->clearAllTimers();
sendJSON(200, "所有定时器已清除");
}
void WebServer::handleGetPins() {
enableCORS();
server.send(200, "application/json", timerManager->getAvailablePinsJSON());
}
void WebServer::handleManualControl() {
enableCORS();
if (!server.hasArg("plain")) {
sendJSON(400, "缺少请求数据", false);
return;
}
DynamicJsonDocument doc(512);
deserializeJson(doc, server.arg("plain"));
int pin = doc["pin"];
int duration = doc["duration"];
bool isPWM = doc["isPWM"].as<bool>();
int pwmValue = doc["pwmValue"].is<int>() ? doc["pwmValue"].as<int>() : 512; // 默认值50%
timerManager->executeManualControl(pin, duration, isPWM, pwmValue);
sendJSON(200, "手动控制执行成功");
}
void WebServer::handleWiFiConfig() {
enableCORS();
if (!server.hasArg("plain")) {
sendJSON(400, "缺少请求数据", false);
return;
}
DynamicJsonDocument doc(512);
deserializeJson(doc, server.arg("plain"));
String ssid = doc["ssid"];
String password = doc["password"];
if (ssid.length() == 0) {
sendJSON(400, "SSID 不能为空", false);
return;
}
// 保存 WiFi 凭据
wifiManager->saveWiFiCredentials(ssid, password);
sendJSON(200, "WiFi 配置已保存,设备将重启");
// 延迟重启以确保响应发送完成
delay(1000);
ESP.restart();
}
void WebServer::handleWiFiReset() {
enableCORS();
// 清除 WiFi 凭据
wifiManager->saveWiFiCredentials("", "");
sendJSON(200, "WiFi 设置已重置,设备将重启为 AP 模式");
// 延迟重启
delay(1000);
ESP.restart();
}
void WebServer::handleRestartAP() {
enableCORS();
// 重启为AP模式清除WiFi凭据并重启
wifiManager->saveWiFiCredentials("", "");
sendJSON(200, "设备将重启为热点模式");
// 延迟重启
delay(1000);
ESP.restart();
}
void WebServer::sendJSON(int code, const String& message, bool success) {
DynamicJsonDocument doc(512);
doc["success"] = success;
doc["message"] = message;
String response;
serializeJson(doc, response);
server.send(code, "application/json", response);
}
void WebServer::sendJSON(DynamicJsonDocument& doc) {
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
void WebServer::enableCORS() {
server.sendHeader("Access-Control-Allow-Origin", "*");
server.sendHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
server.sendHeader("Access-Control-Allow-Headers", "Content-Type");
if (server.method() == HTTP_OPTIONS) {
server.send(204);
}
}
void WebServer::handleGetPWMConfig() {
enableCORS();
DynamicJsonDocument doc(512);
doc["frequency"] = PWM_FREQUENCY;
doc["resolution"] = PWM_RESOLUTION;
doc["maxValue"] = PWM_MAX_VALUE;
doc["minValue"] = 0;
doc["defaultValue"] = PWM_MAX_VALUE / 2; // 50%
sendJSON(doc);
}

51
src/web_server.h Normal file
View File

@@ -0,0 +1,51 @@
#ifndef WEB_SERVER_H
#define WEB_SERVER_H
#include <ESP8266WebServer.h>
#include <ArduinoJson.h>
#include "config.h"
#include "wifi_manager.h"
#include "timer_manager.h"
#include "time_manager.h"
#include "web_pages.h"
class WebServer {
private:
ESP8266WebServer server;
WiFiManager* wifiManager;
TimerManager* timerManager;
TimeManager* timeManager;
public:
WebServer(WiFiManager* wm, TimerManager* tm, TimeManager* timeM);
void begin();
void handleClient();
private:
void setupRoutes();
// 页面路由
void handleRoot();
// API 路由
void handleGetStatus();
void handleGetSystemInfo();
void handleGetTimers();
void handleAddTimer();
void handleUpdateTimer();
void handleDeleteTimer();
void handleClearTimers();
void handleGetPins();
void handleGetPWMConfig();
void handleManualControl();
void handleWiFiConfig();
void handleWiFiReset();
void handleRestartAP();
// 工具函数
void sendJSON(int code, const String& message, bool success = true);
void sendJSON(DynamicJsonDocument& doc);
void enableCORS();
};
#endif

135
src/wifi_manager.cpp Normal file
View File

@@ -0,0 +1,135 @@
#include "wifi_manager.h"
WiFiManager::WiFiManager() {
isAPMode = false;
}
bool WiFiManager::begin() {
EEPROM.begin(EEPROM_SIZE);
savedSSID = loadWiFiSSID();
savedPassword = loadWiFiPassword();
Serial.println("WiFi Manager 初始化");
Serial.println("保存的 SSID: " + savedSSID);
// 如果有保存的 WiFi 信息,尝试连接
if (savedSSID.length() > 0) {
Serial.println("尝试连接到保存的 WiFi...");
if (connectToWiFi(savedSSID, savedPassword)) {
Serial.println("连接成功IP: " + WiFi.localIP().toString());
return true;
}
}
// 连接失败或没有保存的信息,启动 AP 模式
Serial.println("启动 AP 模式");
setupAP();
return false;
}
void WiFiManager::setupAP() {
WiFi.mode(WIFI_AP);
// AP 模式无密码,更容易连接
WiFi.softAP(DEFAULT_AP_SSID);
isAPMode = true;
Serial.println("AP 模式启动");
Serial.println("SSID: " + String(DEFAULT_AP_SSID));
Serial.println("密码: 无密码");
Serial.println("AP IP: " + WiFi.softAPIP().toString());
}
bool WiFiManager::connectToWiFi(const String& ssid, const String& password) {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid.c_str(), password.c_str());
unsigned long startTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startTime < WIFI_TIMEOUT) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
isAPMode = false;
return true;
}
return false;
}
void WiFiManager::saveWiFiCredentials(const String& ssid, const String& password) {
// 清空 EEPROM 区域
for (int i = 0; i < MAX_SSID_LENGTH + MAX_PASSWORD_LENGTH; i++) {
EEPROM.write(WIFI_SSID_ADDR + i, 0);
}
// 保存 SSID
for (int i = 0; i < ssid.length() && i < MAX_SSID_LENGTH - 1; i++) {
EEPROM.write(WIFI_SSID_ADDR + i, ssid[i]);
}
// 保存 Password
for (int i = 0; i < password.length() && i < MAX_PASSWORD_LENGTH - 1; i++) {
EEPROM.write(WIFI_PASSWORD_ADDR + i, password[i]);
}
EEPROM.commit();
savedSSID = ssid;
savedPassword = password;
Serial.println("WiFi 凭据已保存");
}
String WiFiManager::loadWiFiSSID() {
String ssid = "";
for (int i = 0; i < MAX_SSID_LENGTH; i++) {
char c = EEPROM.read(WIFI_SSID_ADDR + i);
if (c == 0) break;
ssid += c;
}
return ssid;
}
String WiFiManager::loadWiFiPassword() {
String password = "";
for (int i = 0; i < MAX_PASSWORD_LENGTH; i++) {
char c = EEPROM.read(WIFI_PASSWORD_ADDR + i);
if (c == 0) break;
password += c;
}
return password;
}
bool WiFiManager::isConnected() {
return !isAPMode && WiFi.status() == WL_CONNECTED;
}
bool WiFiManager::isInAPMode() {
return isAPMode;
}
String WiFiManager::getLocalIP() {
if (isConnected()) {
return WiFi.localIP().toString();
}
return "";
}
String WiFiManager::getAPIP() {
if (isAPMode) {
return WiFi.softAPIP().toString();
}
return "";
}
void WiFiManager::handleWiFiConnection() {
if (!isAPMode && WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi 连接丢失,重新连接...");
if (!connectToWiFi(savedSSID, savedPassword)) {
Serial.println("重连失败,启动 AP 模式");
setupAP();
}
}
}

29
src/wifi_manager.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H
#include <ESP8266WiFi.h>
#include <EEPROM.h>
#include "config.h"
class WiFiManager {
private:
String savedSSID;
String savedPassword;
bool isAPMode;
public:
WiFiManager();
bool begin();
void setupAP();
bool connectToWiFi(const String& ssid, const String& password);
void saveWiFiCredentials(const String& ssid, const String& password);
String loadWiFiSSID();
String loadWiFiPassword();
bool isConnected();
bool isInAPMode();
String getLocalIP();
String getAPIP();
void handleWiFiConnection();
};
#endif

11
test/README Normal file
View File

@@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html