Refactor timer duration to support float values and add firmware update functionality in web server

This commit is contained in:
MiaoMint
2025-08-30 21:03:40 +08:00
parent 48e42aada6
commit 44c707f212
6 changed files with 398 additions and 76 deletions

View File

@@ -40,7 +40,7 @@ struct TimerConfig
int pin; int pin;
int hour; int hour;
int minute; int minute;
int duration; // 持续时间(秒) float duration; // 持续时间(秒,支持小数
bool repeatDaily; // 每天重复 bool repeatDaily; // 每天重复
bool isActive; // 当前是否激活 bool isActive; // 当前是否激活
unsigned long startTime; // 开始时间millis时间戳 unsigned long startTime; // 开始时间millis时间戳

View File

@@ -94,7 +94,7 @@ void TimerManager::update() {
// 检查是否需要关闭 // 检查是否需要关闭
if (timers[i].isActive && if (timers[i].isActive &&
currentTime - timers[i].startTime >= (unsigned long)(timers[i].duration * 1000)) { currentTime - timers[i].startTime >= (unsigned long)(timers[i].duration * 1000.0)) {
timers[i].isActive = false; timers[i].isActive = false;
timers[i].realStartTime = 0; // 清理真实时间戳 timers[i].realStartTime = 0; // 清理真实时间戳
@@ -112,7 +112,7 @@ void TimerManager::update() {
} }
} }
bool TimerManager::addTimer(int pin, int hour, int minute, int duration, bool repeatDaily, bool isPWM, int pwmValue) { bool TimerManager::addTimer(int pin, int hour, int minute, float duration, bool repeatDaily, bool isPWM, int pwmValue) {
if (timerCount >= MAX_TIMERS) { if (timerCount >= MAX_TIMERS) {
return false; return false;
} }
@@ -128,7 +128,7 @@ bool TimerManager::addTimer(int pin, int hour, int minute, int duration, bool re
if (!pinValid) return false; if (!pinValid) return false;
// 验证时间 // 验证时间
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || duration <= 0) { if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || duration <= 0.0) {
return false; return false;
} }
@@ -183,7 +183,7 @@ bool TimerManager::removeTimer(int index) {
return true; return true;
} }
bool TimerManager::updateTimer(int index, int pin, int hour, int minute, int duration, bool enabled, bool repeatDaily, bool isPWM, int pwmValue) { bool TimerManager::updateTimer(int index, int pin, int hour, int minute, float duration, bool enabled, bool repeatDaily, bool isPWM, int pwmValue) {
if (index < 0 || index >= timerCount) { if (index < 0 || index >= timerCount) {
return false; return false;
} }
@@ -199,7 +199,7 @@ bool TimerManager::updateTimer(int index, int pin, int hour, int minute, int dur
if (!pinValid) return false; if (!pinValid) return false;
// 验证时间 // 验证时间
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || duration <= 0) { if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || duration <= 0.0) {
return false; return false;
} }
@@ -234,11 +234,11 @@ bool TimerManager::updateTimer(int index, int pin, int hour, int minute, int dur
} }
String TimerManager::getTimersJSON() { String TimerManager::getTimersJSON() {
DynamicJsonDocument doc(2048); JsonDocument doc;
JsonArray array = doc.to<JsonArray>(); JsonArray array = doc.to<JsonArray>();
for (int i = 0; i < timerCount; i++) { for (int i = 0; i < timerCount; i++) {
JsonObject timer = array.createNestedObject(); JsonObject timer = array.add<JsonObject>();
timer["index"] = i; timer["index"] = i;
timer["enabled"] = timers[i].enabled; timer["enabled"] = timers[i].enabled;
timer["pin"] = timers[i].pin; timer["pin"] = timers[i].pin;
@@ -257,11 +257,11 @@ String TimerManager::getTimersJSON() {
} }
String TimerManager::getAvailablePinsJSON() { String TimerManager::getAvailablePinsJSON() {
DynamicJsonDocument doc(1024); JsonDocument doc;
JsonArray array = doc.to<JsonArray>(); JsonArray array = doc.to<JsonArray>();
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) { for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) {
JsonObject pin = array.createNestedObject(); JsonObject pin = array.add<JsonObject>();
pin["pin"] = AVAILABLE_PINS[i]; pin["pin"] = AVAILABLE_PINS[i];
pin["state"] = digitalRead(AVAILABLE_PINS[i]); pin["state"] = digitalRead(AVAILABLE_PINS[i]);
@@ -281,7 +281,7 @@ String TimerManager::getAvailablePinsJSON() {
return result; return result;
} }
void TimerManager::executeManualControl(int pin, int duration, bool isPWM, int pwmValue) { void TimerManager::executeManualControl(int pin, float duration, bool isPWM, int pwmValue) {
// 验证引脚 // 验证引脚
bool pinValid = false; bool pinValid = false;
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) { for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) {
@@ -297,14 +297,14 @@ void TimerManager::executeManualControl(int pin, int duration, bool isPWM, int p
return; return;
} }
if (duration > 0) { if (duration > 0.0) {
// 开启引脚指定时间 // 开启引脚指定时间
setPin(pin, HIGH, isPWM ? pwmValue : 0); setPin(pin, HIGH, isPWM ? pwmValue : 0);
String modeStr = isPWM ? " PWM模式, 值=" + String(pwmValue) : " 数字模式"; String modeStr = isPWM ? " PWM模式, 值=" + String(pwmValue) : " 数字模式";
Serial.println("手动控制:引脚 " + String(pin) + " 开启 " + String(duration) + "" + modeStr); Serial.println("手动控制:引脚 " + String(pin) + " 开启 " + String(duration) + "" + modeStr);
// 这里应该使用定时器来关闭,简化处理 // 这里应该使用定时器来关闭,简化处理
delay(duration * 1000); delay((unsigned long)(duration * 1000.0));
setPin(pin, LOW, 0); setPin(pin, LOW, 0);
Serial.println("手动控制:引脚 " + String(pin) + " 关闭"); Serial.println("手动控制:引脚 " + String(pin) + " 关闭");
} else { } else {
@@ -339,8 +339,18 @@ void TimerManager::saveTimers() {
EEPROM.write(addr++, timers[i].pin); EEPROM.write(addr++, timers[i].pin);
EEPROM.write(addr++, timers[i].hour); EEPROM.write(addr++, timers[i].hour);
EEPROM.write(addr++, timers[i].minute); EEPROM.write(addr++, timers[i].minute);
EEPROM.write(addr++, (timers[i].duration >> 8) & 0xFF);
EEPROM.write(addr++, timers[i].duration & 0xFF); // 保存float类型的duration4字节
union {
float f;
uint8_t bytes[4];
} durationConverter;
durationConverter.f = timers[i].duration;
EEPROM.write(addr++, durationConverter.bytes[0]);
EEPROM.write(addr++, durationConverter.bytes[1]);
EEPROM.write(addr++, durationConverter.bytes[2]);
EEPROM.write(addr++, durationConverter.bytes[3]);
EEPROM.write(addr++, timers[i].repeatDaily ? 1 : 0); EEPROM.write(addr++, timers[i].repeatDaily ? 1 : 0);
EEPROM.write(addr++, timers[i].isPWM ? 1 : 0); EEPROM.write(addr++, timers[i].isPWM ? 1 : 0);
EEPROM.write(addr++, (timers[i].pwmValue >> 8) & 0xFF); EEPROM.write(addr++, (timers[i].pwmValue >> 8) & 0xFF);
@@ -387,13 +397,22 @@ void TimerManager::loadTimers() {
timers[i].pin = EEPROM.read(addr++); timers[i].pin = EEPROM.read(addr++);
timers[i].hour = EEPROM.read(addr++); timers[i].hour = EEPROM.read(addr++);
timers[i].minute = EEPROM.read(addr++); timers[i].minute = EEPROM.read(addr++);
int durationHigh = EEPROM.read(addr++);
int durationLow = EEPROM.read(addr++); // 读取float类型的duration4字节
timers[i].duration = (durationHigh << 8) | durationLow; union {
float f;
uint8_t bytes[4];
} durationConverter;
durationConverter.bytes[0] = EEPROM.read(addr++);
durationConverter.bytes[1] = EEPROM.read(addr++);
durationConverter.bytes[2] = EEPROM.read(addr++);
durationConverter.bytes[3] = EEPROM.read(addr++);
timers[i].duration = durationConverter.f;
timers[i].repeatDaily = EEPROM.read(addr++) == 1; timers[i].repeatDaily = EEPROM.read(addr++) == 1;
// 检查是否有PWM数据向后兼容 // 检查是否有PWM数据向后兼容
if (addr < EEPROM_SIZE - 16) { // 增加了13个字节的运行时状态数据9+4 if (addr < EEPROM_SIZE - 20) { // 更新了字节数计算duration现在是4字节而不是2字节
timers[i].isPWM = EEPROM.read(addr++) == 1; timers[i].isPWM = EEPROM.read(addr++) == 1;
int pwmHigh = EEPROM.read(addr++); int pwmHigh = EEPROM.read(addr++);
int pwmLow = EEPROM.read(addr++); int pwmLow = EEPROM.read(addr++);
@@ -455,7 +474,7 @@ void TimerManager::loadTimers() {
unsigned long elapsedTime = currentTime - timers[i].startTime; unsigned long elapsedTime = currentTime - timers[i].startTime;
// 检查定时器是否已经超时 // 检查定时器是否已经超时
if (elapsedTime >= (unsigned long)(timers[i].duration * 1000)) { if (elapsedTime >= (unsigned long)(timers[i].duration * 1000.0)) {
// 定时器已经超时,关闭它 // 定时器已经超时,关闭它
timers[i].isActive = false; timers[i].isActive = false;
timers[i].realStartTime = 0; timers[i].realStartTime = 0;
@@ -465,7 +484,7 @@ void TimerManager::loadTimers() {
// 定时器仍然有效,恢复引脚状态 // 定时器仍然有效,恢复引脚状态
setPin(timers[i].pin, HIGH, timers[i].isPWM ? timers[i].pwmValue : 0); setPin(timers[i].pin, HIGH, timers[i].isPWM ? timers[i].pwmValue : 0);
String modeStr = timers[i].isPWM ? " PWM(" + String(timers[i].pwmValue) + ")" : ""; String modeStr = timers[i].isPWM ? " PWM(" + String(timers[i].pwmValue) + ")" : "";
unsigned long remainingTime = (timers[i].duration * 1000) - elapsedTime; unsigned long remainingTime = (unsigned long)(timers[i].duration * 1000.0) - elapsedTime;
Serial.println("恢复定时器 " + String(i) + " 状态,引脚 " + String(timers[i].pin) + " 开启" + modeStr + Serial.println("恢复定时器 " + String(i) + " 状态,引脚 " + String(timers[i].pin) + " 开启" + modeStr +
",剩余时间: " + String(remainingTime / 1000) + ""); ",剩余时间: " + String(remainingTime / 1000) + "");
} }
@@ -492,8 +511,8 @@ void TimerManager::saveTimerStates() {
int addr = TIMER_CONFIG_ADDR + 1; // 跳过定时器数量 int addr = TIMER_CONFIG_ADDR + 1; // 跳过定时器数量
for (int i = 0; i < timerCount; i++) { for (int i = 0; i < timerCount; i++) {
// 跳过基本配置部分 (10字节) // 跳过基本配置部分 (12字节: enabled(1) + pin(1) + hour(1) + minute(1) + duration(4) + repeatDaily(1) + isPWM(1) + pwmValue(2))
addr += 10; addr += 12;
// 更新运行时状态部分 (13字节) // 更新运行时状态部分 (13字节)
EEPROM.write(addr++, timers[i].isActive ? 1 : 0); EEPROM.write(addr++, timers[i].isActive ? 1 : 0);
@@ -545,7 +564,7 @@ TimerConfig TimerManager::getTimer(int index) {
return timers[index]; return timers[index];
} }
TimerConfig empty = {false, 0, 0, 0, 0, false, false, 0, 0UL, false, 0, 0UL}; TimerConfig empty = {false, 0, 0, 0, 0.0, false, false, 0, 0UL, false, 0, 0UL};
return empty; return empty;
} }

View File

@@ -19,9 +19,9 @@ public:
TimerManager(); TimerManager();
void begin(TimeManager* tm); void begin(TimeManager* tm);
void update(); void update();
bool addTimer(int pin, int hour, int minute, int duration, bool repeatDaily = false, bool isPWM = false, int pwmValue = 512); bool addTimer(int pin, int hour, int minute, float duration, bool repeatDaily = false, bool isPWM = false, int pwmValue = 512);
bool removeTimer(int index); 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); bool updateTimer(int index, int pin, int hour, int minute, float duration, bool enabled, bool repeatDaily = false, bool isPWM = false, int pwmValue = 512);
String getTimersJSON(); String getTimersJSON();
void saveTimers(); void saveTimers();
void saveTimerStates(); // 新增:仅保存运行时状态 void saveTimerStates(); // 新增:仅保存运行时状态
@@ -31,7 +31,7 @@ public:
TimerConfig getTimer(int index); TimerConfig getTimer(int index);
void setPin(int pin, bool state, int pwmValue = 0); void setPin(int pin, bool state, int pwmValue = 0);
String getAvailablePinsJSON(); String getAvailablePinsJSON();
void executeManualControl(int pin, int duration, bool isPWM = false, int pwmValue = 512); void executeManualControl(int pin, float duration, bool isPWM = false, int pwmValue = 512);
bool hasValidTime(); bool hasValidTime();
}; };

View File

@@ -371,6 +371,7 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
<button class="tab" onclick="switchTab('manual', this)"></button> <button class="tab" onclick="switchTab('manual', this)"></button>
<button class="tab" onclick="switchTab('system', this)"></button> <button class="tab" onclick="switchTab('system', this)"></button>
<button class="tab" onclick="switchTab('wifi', this)">WiFi设置</button> <button class="tab" onclick="switchTab('wifi', this)">WiFi设置</button>
<button class="tab" onclick="switchTab('firmware', this)"></button>
</nav> </nav>
</div> </div>
@@ -397,7 +398,7 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
</div> </div>
<div> <div>
<label for="timer-duration" class="block text-sm font-medium text-gray-700 mb-1"> ()</label> <label for="timer-duration" class="block text-sm font-medium text-gray-700 mb-1"> ()</label>
<input type="number" id="timer-duration" min="1" max="86400" value="60" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"> <input type="number" id="timer-duration" max="86400" value="0.3" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<input type="checkbox" id="timer-repeat" checked class="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> <input type="checkbox" id="timer-repeat" checked class="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
@@ -460,7 +461,7 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
</div> </div>
<div> <div>
<label for="manual-duration" class="block text-sm font-medium text-gray-700 mb-1"> (0=)</label> <label for="manual-duration" class="block text-sm font-medium text-gray-700 mb-1"> (0=)</label>
<input type="number" id="manual-duration" min="0" max="86400" value="0" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"> <input type="number" id="manual-duration" max="86400" value="0" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div> </div>
<div> <div>
<label class="flex items-center space-x-2"> <label class="flex items-center space-x-2">
@@ -568,6 +569,65 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
</div> </div>
</div> </div>
</div> </div>
<!-- -->
<div id="firmware-content" class="tab-content hidden fade-in">
<div class="max-w-md mx-auto p-5 border rounded-lg bg-gray-50">
<h3 class="text-lg font-semibold mb-4 text-gray-800">🔧 </h3>
<div id="firmware-message" class="hidden mb-4"></div>
<div class="space-y-4">
<div class="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 class="font-medium text-blue-800 mb-2"></h4>
<div class="text-sm text-blue-700">
<div>: <span id="current-version">...</span></div>
<div>: <span id="build-time">...</span></div>
<div>ID: <span id="firmware-chip-id">...</span></div>
</div>
</div>
<div>
<label for="firmware-file" class="block text-sm font-medium text-gray-700 mb-2">
📁 (.bin)
</label>
<input type="file" id="firmware-file" accept=".bin" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
<div class="mt-2 text-xs text-gray-500">
: .bin ( 2MB)
</div>
</div>
<div class="p-3 text-xs text-yellow-800 rounded-lg bg-yellow-50 border border-yellow-200">
2-5
</div>
<div class="space-y-2">
<button id="upload-btn" class="w-full bg-red-600 text-white font-semibold py-3 px-4 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" onclick="startFirmwareUpdate()" disabled>
🚀
</button>
<div id="upload-progress" class="hidden">
<div class="w-full bg-gray-200 rounded-full h-3">
<div id="progress-bar" class="bg-blue-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div class="text-sm text-gray-600 text-center mt-2">
<span id="progress-text">...</span>
</div>
</div>
</div>
<div class="border-t pt-4">
<h4 class="font-medium text-gray-800 mb-2"></h4>
<ul class="text-sm text-gray-600 space-y-1">
<li> </li>
<li> (.bin格式)</li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</main> </main>
@@ -839,7 +899,7 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
pin: parseInt(pin), pin: parseInt(pin),
hour, hour,
minute, minute,
duration: parseInt(duration), duration: parseFloat(duration),
repeatDaily, repeatDaily,
isPWM: isPWM, isPWM: isPWM,
pwmValue: parseInt(pwmValue) pwmValue: parseInt(pwmValue)
@@ -916,7 +976,7 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
try { try {
const body = { const body = {
pin: parseInt(pin), pin: parseInt(pin),
duration: parseInt(duration) || 0, duration: parseFloat(duration) || 0,
isPWM: isPWM, isPWM: isPWM,
pwmValue: parseInt(pwmValue) pwmValue: parseInt(pwmValue)
}; };
@@ -1030,6 +1090,163 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral(
document.getElementById('timer-pwm-percentage').textContent = percentage + '%'; document.getElementById('timer-pwm-percentage').textContent = percentage + '%';
} }
// 固件更新相关函数
document.addEventListener('DOMContentLoaded', function() {
const fileInput = document.getElementById('firmware-file');
const uploadBtn = document.getElementById('upload-btn');
if (fileInput && uploadBtn) {
fileInput.addEventListener('change', function() {
const file = this.files[0];
if (file) {
if (file.size > 2 * 1024 * 1024) { // 2MB限制
showMessage('firmware-message', ' 2MB', 'error');
this.value = '';
uploadBtn.disabled = true;
return;
}
if (!file.name.toLowerCase().endsWith('.bin')) {
showMessage('firmware-message', ' .bin ', 'error');
this.value = '';
uploadBtn.disabled = true;
return;
}
uploadBtn.disabled = false;
showMessage('firmware-message', `: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`, 'info');
} else {
uploadBtn.disabled = true;
}
});
}
// 加载当前固件信息
loadFirmwareInfo();
});
async function loadFirmwareInfo() {
try {
const response = await fetch('/api/system');
if (response.ok) {
const data = await response.json();
document.getElementById('current-version').textContent = `v1.0.0`;
document.getElementById('build-time').textContent = new Date().toLocaleDateString();
document.getElementById('firmware-chip-id').textContent = data.chipId || '';
}
} catch (error) {
console.error(':', error);
}
}
async function startFirmwareUpdate() {
const fileInput = document.getElementById('firmware-file');
const file = fileInput.files[0];
if (!file) {
showMessage('firmware-message', '', 'error');
return;
}
if (!confirm('')) {
return;
}
const uploadBtn = document.getElementById('upload-btn');
const progressDiv = document.getElementById('upload-progress');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
// 禁用上传按钮并显示进度条
uploadBtn.disabled = true;
progressDiv.classList.remove('hidden');
progressBar.style.width = '0%';
progressText.textContent = '...';
const formData = new FormData();
formData.append('firmware', file);
console.log(':', file.name, file.size, 'bytes');
try {
const xhr = new XMLHttpRequest();
// 监听上传进度
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percentComplete + '%';
progressText.textContent = `... ${percentComplete}%`;
}
};
// 监听状态变化
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
progressBar.style.width = '100%';
progressText.textContent = '...';
showMessage('firmware-message', '...', 'success');
// 模拟刷写进度
setTimeout(() => {
progressText.textContent = '...';
}, 1000);
setTimeout(() => {
progressText.textContent = '...';
showMessage('firmware-message', '', 'success');
}, 3000);
setTimeout(() => {
window.location.reload();
}, 8000);
} else {
console.error(':', xhr.status);
let errorMessage = '';
try {
const response = JSON.parse(xhr.responseText);
errorMessage = response.message || errorMessage;
} catch (e) {
errorMessage = ` (${xhr.status})`;
}
showMessage('firmware-message', `: ${errorMessage}`, 'error');
resetUploadUI();
}
}
};
xhr.onerror = function() {
console.error('');
showMessage('firmware-message', '', 'error');
resetUploadUI();
};
xhr.onabort = function() {
console.error('');
showMessage('firmware-message', '', 'error');
resetUploadUI();
};
xhr.open('POST', '/api/firmware/update', true);
console.log(':', '/api/firmware/update');
xhr.send(formData);
} catch (error) {
console.error(':', error);
showMessage('firmware-message', ': ' + error.message, 'error');
resetUploadUI();
}
}
function resetUploadUI() {
const uploadBtn = document.getElementById('upload-btn');
const progressDiv = document.getElementById('upload-progress');
const fileInput = document.getElementById('firmware-file');
uploadBtn.disabled = false;
progressDiv.classList.add('hidden');
fileInput.value = '';
}
function showMessage(elementId, message, type) { function showMessage(elementId, message, type) {
const el = document.getElementById(elementId); const el = document.getElementById(elementId);
el.textContent = message; el.textContent = message;

View File

@@ -1,4 +1,8 @@
#include "web_server.h" #include "web_server.h"
#include <ESP8266HTTPUpdateServer.h>
#include <Updater.h>
ESP8266HTTPUpdateServer httpUpdater;
WebServer::WebServer(WiFiManager* wm, TimerManager* tm, TimeManager* timeM) : server(WEB_SERVER_PORT) { WebServer::WebServer(WiFiManager* wm, TimerManager* tm, TimeManager* timeM) : server(WEB_SERVER_PORT) {
wifiManager = wm; wifiManager = wm;
@@ -7,6 +11,9 @@ WebServer::WebServer(WiFiManager* wm, TimerManager* tm, TimeManager* timeM) : se
} }
void WebServer::begin() { void WebServer::begin() {
// 设置最大上传文件大小为 2MB
server.setContentLength(2 * 1024 * 1024);
setupRoutes(); setupRoutes();
server.begin(); server.begin();
Serial.println("Web 服务器启动,端口: " + String(WEB_SERVER_PORT)); Serial.println("Web 服务器启动,端口: " + String(WEB_SERVER_PORT));
@@ -33,6 +40,36 @@ void WebServer::setupRoutes() {
server.on("/api/wifi/reset", HTTP_POST, [this]() { handleWiFiReset(); }); server.on("/api/wifi/reset", HTTP_POST, [this]() { handleWiFiReset(); });
server.on("/api/restart-ap", HTTP_POST, [this]() { handleRestartAP(); }); server.on("/api/restart-ap", HTTP_POST, [this]() { handleRestartAP(); });
// 固件更新测试端点
server.on("/api/firmware/info", HTTP_GET, [this]() {
enableCORS();
JsonDocument doc;
doc["version"] = "1.0.0";
doc["buildTime"] = __DATE__ " " __TIME__;
doc["chipId"] = String(ESP.getChipId(), HEX);
doc["flashSize"] = ESP.getFlashChipSize();
doc["freeSpace"] = ESP.getFreeSketchSpace();
sendJSON(doc);
});
server.on("/api/firmware/update", HTTP_POST,
[this]() {
// 处理上传完成后的响应
enableCORS();
if (Update.hasError()) {
sendJSON(500, "固件更新失败", false);
} else {
sendJSON(200, "固件更新成功,设备即将重启");
delay(1000);
ESP.restart();
}
},
[this]() {
// 处理文件上传过程
handleFirmwareUpdate();
}
);
// 404 处理 - 这里会处理动态路由 // 404 处理 - 这里会处理动态路由
server.onNotFound([this]() { server.onNotFound([this]() {
String uri = server.uri(); String uri = server.uri();
@@ -68,7 +105,7 @@ void WebServer::handleRoot() {
void WebServer::handleGetStatus() { void WebServer::handleGetStatus() {
enableCORS(); enableCORS();
DynamicJsonDocument doc(1024); JsonDocument doc;
doc["wifiConnected"] = wifiManager->isConnected(); doc["wifiConnected"] = wifiManager->isConnected();
doc["localIP"] = wifiManager->getLocalIP(); doc["localIP"] = wifiManager->getLocalIP();
doc["apIP"] = wifiManager->getAPIP(); doc["apIP"] = wifiManager->getAPIP();
@@ -95,7 +132,7 @@ void WebServer::handleGetStatus() {
void WebServer::handleGetSystemInfo() { void WebServer::handleGetSystemInfo() {
enableCORS(); enableCORS();
DynamicJsonDocument doc(2048); JsonDocument doc;
// 基本系统信息 // 基本系统信息
doc["chipId"] = String(ESP.getChipId(), HEX); doc["chipId"] = String(ESP.getChipId(), HEX);
@@ -121,48 +158,52 @@ void WebServer::handleGetSystemInfo() {
doc["uptimeMinutes"] = (uptime / (60 * 1000UL)) % 60; doc["uptimeMinutes"] = (uptime / (60 * 1000UL)) % 60;
doc["uptimeSeconds"] = (uptime / 1000UL) % 60; doc["uptimeSeconds"] = (uptime / 1000UL) % 60;
// WiFi 详细信息 // WiFi 信息
JsonObject wifi = doc.createNestedObject("wifi"); JsonObject wifi = doc["wifi"].to<JsonObject>();
wifi["status"] = WiFi.status(); wifi["connected"] = wifiManager->isConnected();
wifi["statusText"] = wifiManager->isConnected() ? "已连接" : (wifiManager->isInAPMode() ? "AP模式" : "断开连接"); wifi["localIP"] = wifiManager->getLocalIP();
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["macAddress"] = WiFi.macAddress();
wifi["channel"] = WiFi.channel(); wifi["apIP"] = wifiManager->getAPIP();
wifi["autoConnect"] = WiFi.getAutoConnect(); wifi["isAPMode"] = wifiManager->isInAPMode();
}
if (wifiManager->isInAPMode()) { // WiFi状态信息
wifi["apSSID"] = WiFi.softAPSSID(); wifi["status"] = WiFi.status();
wifi["apIP"] = WiFi.softAPIP().toString(); if (wifiManager->isConnected()) {
wifi["apMacAddress"] = WiFi.softAPmacAddress(); wifi["statusText"] = "已连接";
wifi["modeText"] = "Station模式";
wifi["rssi"] = WiFi.RSSI();
wifi["ssid"] = WiFi.SSID();
} else if (wifiManager->isInAPMode()) {
wifi["statusText"] = "AP模式";
wifi["modeText"] = "热点模式";
wifi["rssi"] = 0;
wifi["ssid"] = "未连接";
wifi["apSSID"] = DEFAULT_AP_SSID;
wifi["apStationCount"] = WiFi.softAPgetStationNum(); wifi["apStationCount"] = WiFi.softAPgetStationNum();
} else {
wifi["statusText"] = "未连接";
wifi["modeText"] = "未知";
wifi["rssi"] = 0;
wifi["ssid"] = "未连接";
} }
// 时间信息 // 时间信息
JsonObject timeInfo = doc.createNestedObject("time"); JsonObject timeInfo = doc["time"].to<JsonObject>();
timeInfo["hasValidTime"] = timeManager->isTimeValid();
timeInfo["timeSource"] = timeManager->isTimeValid() ? "NTP" : "系统运行时间";
timeInfo["currentTime"] = timeManager->getCurrentTimeString(); timeInfo["currentTime"] = timeManager->getCurrentTimeString();
timeInfo["currentDate"] = timeManager->getCurrentDateString(); timeInfo["currentDate"] = timeManager->getCurrentDateString();
timeInfo["isWiFiTimeAvailable"] = timeManager->isWiFiTimeAvailable(); timeInfo["hasValidTime"] = timeManager->isTimeValid();
timeInfo["isValid"] = timeManager->isTimeValid();
timeInfo["timeSource"] = timeManager->isTimeValid() ? "NTP网络时间" : "系统运行时间";
timeInfo["ntpEnabled"] = wifiManager->isConnected();
timeInfo["uptime"] = millis() / 1000; // 运行时间(秒)
// 引脚状态 // 引脚状态
JsonArray pins = doc.createNestedArray("pins"); JsonArray pins = doc["pins"].to<JsonArray>();
for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) { for (int i = 0; i < AVAILABLE_PINS_COUNT; i++) {
JsonObject pin = pins.createNestedObject(); JsonObject pin = pins.add<JsonObject>();
pin["pin"] = AVAILABLE_PINS[i]; pin["pin"] = AVAILABLE_PINS[i]; // 前端期望的字段名
pin["number"] = AVAILABLE_PINS[i]; // 保持兼容
pin["state"] = digitalRead(AVAILABLE_PINS[i]); pin["state"] = digitalRead(AVAILABLE_PINS[i]);
// 检查是否被定时器占用 // 检查是否被定时器占用
bool inUse = false; bool inUse = false;
int timerIndex = -1; int timerIndex = -1;
@@ -179,7 +220,7 @@ void WebServer::handleGetSystemInfo() {
} }
// 定时器统计 // 定时器统计
JsonObject timerStats = doc.createNestedObject("timerStats"); JsonObject timerStats = doc["timerStats"].to<JsonObject>();
timerStats["total"] = timerManager->getTimerCount(); timerStats["total"] = timerManager->getTimerCount();
int enabled = 0, active = 0, repeatDaily = 0, oneTime = 0; int enabled = 0, active = 0, repeatDaily = 0, oneTime = 0;
@@ -198,12 +239,12 @@ void WebServer::handleGetSystemInfo() {
timerStats["maxTimers"] = MAX_TIMERS; timerStats["maxTimers"] = MAX_TIMERS;
// EEPROM 使用情况 // EEPROM 使用情况
JsonObject eeprom = doc.createNestedObject("eeprom"); JsonObject eeprom = doc["eeprom"].to<JsonObject>();
eeprom["size"] = EEPROM_SIZE; eeprom["size"] = EEPROM_SIZE;
eeprom["wifiConfigStart"] = WIFI_SSID_ADDR; eeprom["wifiConfigStart"] = WIFI_SSID_ADDR;
eeprom["wifiConfigSize"] = TIMER_CONFIG_ADDR - WIFI_SSID_ADDR; eeprom["wifiConfigSize"] = TIMER_CONFIG_ADDR - WIFI_SSID_ADDR;
eeprom["timerConfigStart"] = TIMER_CONFIG_ADDR; eeprom["timerConfigStart"] = TIMER_CONFIG_ADDR;
eeprom["timerConfigUsed"] = 1 + (timerManager->getTimerCount() * 7); // 1 byte count + 7 bytes per timer eeprom["timerConfigUsed"] = 1 + (timerManager->getTimerCount() * 25); // 1 byte count + 25 bytes per timer (12 config + 13 runtime)
sendJSON(doc); sendJSON(doc);
} }
@@ -221,13 +262,13 @@ void WebServer::handleAddTimer() {
return; return;
} }
DynamicJsonDocument doc(1024); JsonDocument doc;
deserializeJson(doc, server.arg("plain")); deserializeJson(doc, server.arg("plain"));
int pin = doc["pin"]; int pin = doc["pin"];
int hour = doc["hour"]; int hour = doc["hour"];
int minute = doc["minute"]; int minute = doc["minute"];
int duration = doc["duration"]; float duration = doc["duration"];
bool repeatDaily = doc["repeatDaily"].as<bool>(); bool repeatDaily = doc["repeatDaily"].as<bool>();
bool isPWM = doc["isPWM"].as<bool>(); bool isPWM = doc["isPWM"].as<bool>();
int pwmValue = doc["pwmValue"].is<int>() ? doc["pwmValue"].as<int>() : 512; // 默认值50% int pwmValue = doc["pwmValue"].is<int>() ? doc["pwmValue"].as<int>() : 512; // 默认值50%
@@ -256,13 +297,13 @@ void WebServer::handleUpdateTimer() {
return; return;
} }
DynamicJsonDocument doc(1024); JsonDocument doc;
deserializeJson(doc, server.arg("plain")); deserializeJson(doc, server.arg("plain"));
int pin = doc["pin"]; int pin = doc["pin"];
int hour = doc["hour"]; int hour = doc["hour"];
int minute = doc["minute"]; int minute = doc["minute"];
int duration = doc["duration"]; float duration = doc["duration"];
bool enabled = doc["enabled"]; bool enabled = doc["enabled"];
bool repeatDaily = doc["repeatDaily"].as<bool>(); bool repeatDaily = doc["repeatDaily"].as<bool>();
bool isPWM = doc["isPWM"].as<bool>(); bool isPWM = doc["isPWM"].as<bool>();
@@ -314,11 +355,11 @@ void WebServer::handleManualControl() {
return; return;
} }
DynamicJsonDocument doc(512); JsonDocument doc;
deserializeJson(doc, server.arg("plain")); deserializeJson(doc, server.arg("plain"));
int pin = doc["pin"]; int pin = doc["pin"];
int duration = doc["duration"]; float duration = doc["duration"];
bool isPWM = doc["isPWM"].as<bool>(); bool isPWM = doc["isPWM"].as<bool>();
int pwmValue = doc["pwmValue"].is<int>() ? doc["pwmValue"].as<int>() : 512; // 默认值50% int pwmValue = doc["pwmValue"].is<int>() ? doc["pwmValue"].as<int>() : 512; // 默认值50%
@@ -334,7 +375,7 @@ void WebServer::handleWiFiConfig() {
return; return;
} }
DynamicJsonDocument doc(512); JsonDocument doc;
deserializeJson(doc, server.arg("plain")); deserializeJson(doc, server.arg("plain"));
String ssid = doc["ssid"]; String ssid = doc["ssid"];
@@ -382,7 +423,7 @@ void WebServer::handleRestartAP() {
} }
void WebServer::sendJSON(int code, const String& message, bool success) { void WebServer::sendJSON(int code, const String& message, bool success) {
DynamicJsonDocument doc(512); JsonDocument doc;
doc["success"] = success; doc["success"] = success;
doc["message"] = message; doc["message"] = message;
@@ -392,7 +433,7 @@ void WebServer::sendJSON(int code, const String& message, bool success) {
server.send(code, "application/json", response); server.send(code, "application/json", response);
} }
void WebServer::sendJSON(DynamicJsonDocument& doc) { void WebServer::sendJSON(JsonDocument& doc) {
String response; String response;
serializeJson(doc, response); serializeJson(doc, response);
server.send(200, "application/json", response); server.send(200, "application/json", response);
@@ -411,7 +452,7 @@ void WebServer::enableCORS() {
void WebServer::handleGetPWMConfig() { void WebServer::handleGetPWMConfig() {
enableCORS(); enableCORS();
DynamicJsonDocument doc(512); JsonDocument doc;
doc["frequency"] = PWM_FREQUENCY; doc["frequency"] = PWM_FREQUENCY;
doc["resolution"] = PWM_RESOLUTION; doc["resolution"] = PWM_RESOLUTION;
doc["maxValue"] = PWM_MAX_VALUE; doc["maxValue"] = PWM_MAX_VALUE;
@@ -420,3 +461,47 @@ void WebServer::handleGetPWMConfig() {
sendJSON(doc); sendJSON(doc);
} }
void WebServer::handleFirmwareUpdate() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("固件更新开始: %s\n", upload.filename.c_str());
// 检查文件扩展名
if (!upload.filename.endsWith(".bin")) {
Serial.println("错误:不支持的文件格式");
return;
}
// 开始OTA更新
uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
if (!Update.begin(maxSketchSpace)) {
Serial.println("错误:无法开始固件更新");
Update.printError(Serial);
return;
}
Serial.println("固件更新已开始...");
}
else if (upload.status == UPLOAD_FILE_WRITE) {
// 写入固件数据
Serial.printf("写入 %u 字节...\n", upload.currentSize);
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Serial.println("错误:固件写入失败");
Update.printError(Serial);
}
}
else if (upload.status == UPLOAD_FILE_END) {
// 完成固件更新
if (Update.end(true)) {
Serial.printf("固件更新成功: %u 字节\n", upload.totalSize);
} else {
Serial.println("错误:固件更新完成失败");
Update.printError(Serial);
}
}
else if (upload.status == UPLOAD_FILE_ABORTED) {
Update.end();
Serial.println("固件更新被中止");
}
}

View File

@@ -41,10 +41,11 @@ private:
void handleWiFiConfig(); void handleWiFiConfig();
void handleWiFiReset(); void handleWiFiReset();
void handleRestartAP(); void handleRestartAP();
void handleFirmwareUpdate();
// 工具函数 // 工具函数
void sendJSON(int code, const String& message, bool success = true); void sendJSON(int code, const String& message, bool success = true);
void sendJSON(DynamicJsonDocument& doc); void sendJSON(JsonDocument& doc);
void enableCORS(); void enableCORS();
}; };