init
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal 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
10
.vscode/extensions.json
vendored
Normal 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
70
PWM_README.md
Normal 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
267
README.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# PetIO 控制系统
|
||||||
|
|
||||||
|
一个基于 ESP8266 的智能引脚定时控制系统,支持 Web 界面管理和 WiFi 配置。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 🎯 定时器管理
|
||||||
|
- ✅ 设置任意引脚在指定时间点开启指定时长
|
||||||
|
- ✅ 支持多个定时器同时运行
|
||||||
|
- ✅ 可启用/禁用单个定时器
|
||||||
|
- ✅ **每天重复功能** - 可设置定时器每天自动重复
|
||||||
|
- ✅ **单次执行功能** - 执行一次后自动禁用
|
||||||
|
- ✅ 定时器状态实时显示
|
||||||
|
- ✅ 持### 3. 定时器不工作
|
||||||
|
- 确认设备已连接 WiFi(AP 模式下定时器功能受限)
|
||||||
|
- 检查系统时间是否已同步(状态页面会显示时间来源)
|
||||||
|
- 确认定时器已启用且设置正确
|
||||||
|
- 检查引脚连接
|
||||||
|
|
||||||
|
### 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
37
include/README
Normal 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
46
lib/README
Normal 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
24
platformio.ini
Normal 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
53
src/config.h
Normal 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
137
src/display_manager.cpp
Normal 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
47
src/display_manager.h
Normal 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
131
src/main.cpp
Normal 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
135
src/time_manager.cpp
Normal 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
31
src/time_manager.h
Normal 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
560
src/timer_manager.cpp
Normal 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
38
src/timer_manager.h
Normal 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
1050
src/web_pages.h
Normal file
File diff suppressed because it is too large
Load Diff
422
src/web_server.cpp
Normal file
422
src/web_server.cpp
Normal 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
51
src/web_server.h
Normal 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
135
src/wifi_manager.cpp
Normal 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
29
src/wifi_manager.h
Normal 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
11
test/README
Normal 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
|
||||||
Reference in New Issue
Block a user