commit 4dcbb9c05b2dd3195927106c64b9a6e33b246eea Author: MiaoMint <44718819+MiaoMint@users.noreply.github.com> Date: Tue Aug 12 04:24:15 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -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" + ] +} diff --git a/PWM_README.md b/PWM_README.md new file mode 100644 index 0000000..3788e34 --- /dev/null +++ b/PWM_README.md @@ -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. 旧的定时器配置会自动设置为数字模式 diff --git a/README.md b/README.md new file mode 100644 index 0000000..28eded0 --- /dev/null +++ b/README.md @@ -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 控制系统!** 🐾 diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -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 diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -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 +#include + +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 diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..660ee13 --- /dev/null +++ b/platformio.ini @@ -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 \ No newline at end of file diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..f73b7a2 --- /dev/null +++ b/src/config.h @@ -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 diff --git a/src/display_manager.cpp b/src/display_manager.cpp new file mode 100644 index 0000000..00053e8 --- /dev/null +++ b/src/display_manager.cpp @@ -0,0 +1,137 @@ +#include "display_manager.h" +#include + +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()); +} \ No newline at end of file diff --git a/src/display_manager.h b/src/display_manager.h new file mode 100644 index 0000000..24605be --- /dev/null +++ b/src/display_manager.h @@ -0,0 +1,47 @@ +#ifndef DISPLAY_MANAGER_H +#define DISPLAY_MANAGER_H + +#include +#include +#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 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..bb98263 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,131 @@ +#include +#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(); +} diff --git a/src/time_manager.cpp b/src/time_manager.cpp new file mode 100644 index 0000000..cce5dee --- /dev/null +++ b/src/time_manager.cpp @@ -0,0 +1,135 @@ +#include "time_manager.h" +#include + +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 +} diff --git a/src/time_manager.h b/src/time_manager.h new file mode 100644 index 0000000..1ba2c9d --- /dev/null +++ b/src/time_manager.h @@ -0,0 +1,31 @@ +#ifndef TIME_MANAGER_H +#define TIME_MANAGER_H + +#include +#include +#include +#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 diff --git a/src/timer_manager.cpp b/src/timer_manager.cpp new file mode 100644 index 0000000..23504e1 --- /dev/null +++ b/src/timer_manager.cpp @@ -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(); + + 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(); + + 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); + } +} diff --git a/src/timer_manager.h b/src/timer_manager.h new file mode 100644 index 0000000..749c888 --- /dev/null +++ b/src/timer_manager.h @@ -0,0 +1,38 @@ +#ifndef TIMER_MANAGER_H +#define TIMER_MANAGER_H + +#include +#include +#include +#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 diff --git a/src/web_pages.h b/src/web_pages.h new file mode 100644 index 0000000..e1d55e1 --- /dev/null +++ b/src/web_pages.h @@ -0,0 +1,1050 @@ +#ifndef WEB_PAGES_H +#define WEB_PAGES_H + +// AP模式专用页面 - 无需外部CSS,仅WiFi设置 +const char AP_MODE_HTML[] PROGMEM = R"rawliteral( + + + + + + PetIO WiFi设置 + + + +
+
+

🐾 PetIO

+

WiFi网络配置

+
+ +
+
+ 📶 AP模式 - 需要连接WiFi +
+ + + +
+ 💡 设备当前处于热点模式。请连接到您的WiFi网络以启用完整功能,包括定时器和时间同步。 +
+ +
+
+ + +
+ +
+ + +
+ + + + +
+
+ + +
+ + + + +)rawliteral"; + +const char INDEX_HTML[] PROGMEM = R"rawliteral( + + + + + + PetIO 控制面板 + + + + + +
+ +
+
+
+

🐾 PetIO 控制面板

+

一个智能宠物喂养定时器

+
+
+ + 检测中... + +
+
+
+
+ +
+
+
+ +
+ +
+ +
+ + +
+
+
+

添加新定时器

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ +
+
+

现有定时器

+
+
加载中...
+
+
+
+
+ + + + + + + + + +
+
+
+
+ + + + +)rawliteral"; + +#endif diff --git a/src/web_server.cpp b/src/web_server.cpp new file mode 100644 index 0000000..b049798 --- /dev/null +++ b/src/web_server.cpp @@ -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 isPWM = doc["isPWM"].as(); + int pwmValue = doc["pwmValue"].is() ? doc["pwmValue"].as() : 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 isPWM = doc["isPWM"].as(); + int pwmValue = doc["pwmValue"].is() ? doc["pwmValue"].as() : 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(); + int pwmValue = doc["pwmValue"].is() ? doc["pwmValue"].as() : 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); +} diff --git a/src/web_server.h b/src/web_server.h new file mode 100644 index 0000000..b038684 --- /dev/null +++ b/src/web_server.h @@ -0,0 +1,51 @@ +#ifndef WEB_SERVER_H +#define WEB_SERVER_H + +#include +#include +#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 diff --git a/src/wifi_manager.cpp b/src/wifi_manager.cpp new file mode 100644 index 0000000..f3bc918 --- /dev/null +++ b/src/wifi_manager.cpp @@ -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(); + } + } +} diff --git a/src/wifi_manager.h b/src/wifi_manager.h new file mode 100644 index 0000000..cccf0d0 --- /dev/null +++ b/src/wifi_manager.h @@ -0,0 +1,29 @@ +#ifndef WIFI_MANAGER_H +#define WIFI_MANAGER_H + +#include +#include +#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 diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -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