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