Files
Dorm-Air-Conditioner-Smart-…/Dorm-Air-Conditioner-Smart-Controller.ino
2025-09-23 23:58:49 +08:00

881 lines
32 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//使用preferences作为数据持久化方案,类似于本项目的数据库
#include <Preferences.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
#include "DHT11control.h"
#include "RTC_Module.h"
#include <RtcDS1302.h>
#include "LunarCalendarAndHolidayJudge.h"
#include "core.h"
#include "ir_control.h"
// WiFi热点配置
const char* ap_ssid = "ESP32_AirConditioner";
const char* ap_password = "12345678";
// Web服务器
WebServer server(80);
// 全局变量
ThreeWire myWire(4,5,2);
RtcDS1302<ThreeWire> Rtc(myWire);
Preferences prefs;
bool isRecording = false;
unsigned long lastJudgeTime = 0;
const unsigned long judgeInterval = 30000; // 30秒执行一次判断
// 存储自定义按钮的红外信号
struct CustomButton {
String name;
String irData;
bool isValid;
};
// 最多支持10个自定义按钮
CustomButton customButtons[10];
int customButtonCount = 0;
void setup() {
// 初始化串口通信
Serial.begin(115200);
// 打开preferences命名空间
prefs.begin("DACSC");
// 初始化红外控制
initIRControl();
// 启动DHT传感器
dht.begin();
// 启动RTC
setupRTC();
// 初始化核心模块
Serial.println("正在初始化智能空调控制系统...");
if (initializeCore()) {
Serial.println("系统初始化成功!");
} else {
Serial.println("系统初始化失败!");
}
// 加载自定义按钮
loadCustomButtons();
// 设置WiFi热点
setupWiFiAP();
// 设置Web服务器路由
setupWebServer();
// 启动Web服务器
server.begin();
Serial.println("Web服务器已启动");
Serial.print("请连接WiFi: ");
Serial.println(ap_ssid);
Serial.print("密码: ");
Serial.println(ap_password);
Serial.print("然后访问: http://");
Serial.println(WiFi.softAPIP());
}
void loop() {
// 处理Web服务器请求
server.handleClient();
// 定期执行核心判断逻辑
unsigned long currentTime = millis();
if (currentTime - lastJudgeTime >= judgeInterval) {
lastJudgeTime = currentTime;
executeJudgeLogic();
}
delay(10);
}
// 设置WiFi热点
void setupWiFiAP() {
Serial.println("正在设置WiFi热点...");
WiFi.mode(WIFI_AP);
WiFi.softAP(ap_ssid, ap_password);
IPAddress IP = WiFi.softAPIP();
Serial.print("热点IP地址: ");
Serial.println(IP);
Serial.println("WiFi热点设置完成");
}
// 设置Web服务器路由
void setupWebServer() {
// 主页
server.on("/", handleRoot);
// API路由
server.on("/api/time", HTTP_GET, handleGetTime);
server.on("/api/time", HTTP_POST, handleSetTime);
server.on("/api/buttons", HTTP_GET, handleGetButtons);
server.on("/api/buttons", HTTP_POST, handleAddButton);
server.on("/api/buttons", HTTP_DELETE, handleDeleteButton);
server.on("/api/record", HTTP_POST, handleStartRecord);
server.on("/api/send", HTTP_POST, handleSendIR);
server.on("/api/settings", HTTP_GET, handleGetSettings);
server.on("/api/settings", HTTP_POST, handleSetSettings);
// 404处理
server.onNotFound(handleNotFound);
}
// 执行核心判断逻辑
void executeJudgeLogic() {
int judgeResult = judge();
switch (judgeResult) {
case JUDGE_TURN_ON_COOLING:
Serial.println("执行操作: 开启制冷");
sendStoredIRSignal("制冷");
break;
case JUDGE_TURN_ON_HEATING:
Serial.println("执行操作: 开启制暖");
sendStoredIRSignal("制热");
break;
case JUDGE_TURN_OFF_AC:
Serial.println("执行操作: 关闭空调");
sendStoredIRSignal("关机");
break;
case JUDGE_ADJUST_TEMP:
Serial.println("执行操作: 除湿");
sendStoredIRSignal("除湿");
break;
case JUDGE_NO_ACTION:
Serial.println("执行操作: 无需操作");
break;
case JUDGE_ERROR:
Serial.println("执行操作: 判断出错");
break;
default:
Serial.println("执行操作: 未知结果");
break;
}
}
// 发送存储的红外信号
void sendStoredIRSignal(String buttonName) {
String irDataStr = prefs.getString(("ir_" + buttonName).c_str(), "");
if (irDataStr.length() == 0) {
Serial.println("未找到按钮 " + buttonName + " 的红外信号");
return;
}
// 解析红外信号数据
IRSignal signal = parseIRSignalFromString(irDataStr);
if (signal.isValid) {
Serial.println("发送红外信号: " + buttonName);
sendIRSignal(signal);
freeIRSignal(signal);
} else {
Serial.println("红外信号数据无效: " + buttonName);
}
}
// 从字符串解析红外信号
IRSignal parseIRSignalFromString(String dataStr) {
IRSignal signal;
signal.markTimes = nullptr;
signal.spaceTimes = nullptr;
signal.markCount = 0;
signal.spaceCount = 0;
signal.carrierFreq = IR_CARRIER_FREQ;
signal.isValid = false;
if (dataStr.length() == 0) {
return signal;
}
// 计算数据长度
int commaCount = 0;
for (int i = 0; i < dataStr.length(); i++) {
if (dataStr[i] == ',') commaCount++;
}
int totalLength = commaCount + 1;
// 分配临时内存存储原始数据
unsigned int* tempData = (unsigned int*)malloc(totalLength * sizeof(unsigned int));
if (tempData == nullptr) {
return signal;
}
// 解析数据
int index = 0;
int startPos = 0;
for (int i = 0; i <= dataStr.length(); i++) {
if (i == dataStr.length() || dataStr[i] == ',') {
String valueStr = dataStr.substring(startPos, i);
tempData[index] = valueStr.toInt();
index++;
startPos = i + 1;
}
}
// 分离mark和space数据
signal.markCount = (totalLength + 1) / 2;
signal.spaceCount = totalLength / 2;
// 分配mark时间数组
if (signal.markCount > 0) {
signal.markTimes = (unsigned int*)malloc(signal.markCount * sizeof(unsigned int));
if (signal.markTimes == nullptr) {
free(tempData);
return signal;
}
for (int i = 0; i < signal.markCount; i++) {
signal.markTimes[i] = tempData[i * 2];
}
}
// 分配space时间数组
if (signal.spaceCount > 0) {
signal.spaceTimes = (unsigned int*)malloc(signal.spaceCount * sizeof(unsigned int));
if (signal.spaceTimes == nullptr) {
if (signal.markTimes != nullptr) {
free(signal.markTimes);
signal.markTimes = nullptr;
signal.markCount = 0;
}
free(tempData);
return signal;
}
for (int i = 0; i < signal.spaceCount; i++) {
signal.spaceTimes[i] = tempData[i * 2 + 1];
}
}
free(tempData);
signal.isValid = true;
return signal;
}
// 将红外信号转换为字符串
String irSignalToString(const IRSignal& signal) {
if (!signal.isValid ||
(signal.markTimes == nullptr && signal.spaceTimes == nullptr) ||
(signal.markCount == 0 && signal.spaceCount == 0)) {
return "";
}
String result = "";
int maxCount = max(signal.markCount, signal.spaceCount);
// 重建原始时序数据交替输出mark和space
for (int i = 0; i < maxCount; i++) {
// 添加mark时间
if (i < signal.markCount) {
if (result.length() > 0) result += ",";
result += String(signal.markTimes[i]);
}
// 添加space时间
if (i < signal.spaceCount) {
if (result.length() > 0) result += ",";
result += String(signal.spaceTimes[i]);
}
}
return result;
}
// 加载自定义按钮
void loadCustomButtons() {
customButtonCount = prefs.getInt("customBtnCount", 0);
for (int i = 0; i < customButtonCount && i < 10; i++) {
String keyName = "customBtn_" + String(i) + "_name";
String keyData = "customBtn_" + String(i) + "_data";
customButtons[i].name = prefs.getString(keyName.c_str(), "");
customButtons[i].irData = prefs.getString(keyData.c_str(), "");
customButtons[i].isValid = (customButtons[i].name.length() > 0 && customButtons[i].irData.length() > 0);
}
}
// 保存自定义按钮
void saveCustomButtons() {
prefs.putInt("customBtnCount", customButtonCount);
for (int i = 0; i < customButtonCount && i < 10; i++) {
String keyName = "customBtn_" + String(i) + "_name";
String keyData = "customBtn_" + String(i) + "_data";
prefs.putString(keyName.c_str(), customButtons[i].name);
prefs.putString(keyData.c_str(), customButtons[i].irData);
}
}
// Web服务器处理函数
// 主页处理
void handleRoot() {
String html = "<!DOCTYPE html>";
html += "<html lang=\"zh-CN\">";
html += "<head>";
html += "<meta charset=\"UTF-8\">";
html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
html += "<title>宿舍空调智能控制器</title>";
html += "<style>";
html += "* { margin: 0; padding: 0; box-sizing: border-box; }";
html += "body { font-family: 'Microsoft YaHei', Arial, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }";
html += ".container { max-width: 800px; margin: 0 auto; padding: 20px; }";
html += ".header { background: rgba(255,255,255,0.95); border-radius: 15px; padding: 20px; margin-bottom: 20px; text-align: center; box-shadow: 0 8px 32px rgba(0,0,0,0.1); }";
html += ".time-display { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 10px; }";
html += ".date-display { font-size: 16px; color: #666; }";
html += ".main-content { background: rgba(255,255,255,0.95); border-radius: 15px; padding: 30px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); }";
html += ".section { margin-bottom: 30px; }";
html += ".section-title { font-size: 20px; font-weight: bold; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 5px; }";
html += ".button-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 20px; }";
html += ".btn { padding: 15px 20px; border: none; border-radius: 10px; font-size: 16px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; text-align: center; }";
html += ".btn-primary { background: linear-gradient(45deg, #667eea, #764ba2); color: white; }";
html += ".btn-primary:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102,126,234,0.4); }";
html += ".btn-success { background: linear-gradient(45deg, #56ab2f, #a8e6cf); color: white; }";
html += ".btn-success:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(86,171,47,0.4); }";
html += ".btn-danger { background: linear-gradient(45deg, #ff416c, #ff4b2b); color: white; }";
html += ".btn-danger:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(255,65,108,0.4); }";
html += ".btn-warning { background: linear-gradient(45deg, #f7971e, #ffd200); color: white; }";
html += ".btn-warning:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(247,151,30,0.4); }";
html += ".btn-secondary { background: linear-gradient(45deg, #bdc3c7, #2c3e50); color: white; }";
html += ".btn-secondary:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(189,195,199,0.4); }";
html += ".control-panel { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }";
html += ".input-group { display: flex; gap: 10px; margin-bottom: 15px; align-items: center; }";
html += ".input-group input { flex: 1; padding: 10px; border: 2px solid #ddd; border-radius: 8px; font-size: 14px; }";
html += ".input-group input:focus { outline: none; border-color: #667eea; }";
html += ".status { padding: 10px; border-radius: 8px; margin-bottom: 15px; font-weight: bold; }";
html += ".status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }";
html += ".status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }";
html += ".status.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }";
html += ".modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }";
html += ".modal-content { background-color: white; margin: 10% auto; padding: 30px; border-radius: 15px; width: 90%; max-width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }";
html += ".close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }";
html += ".close:hover { color: #000; }";
html += "@media (max-width: 600px) { .container { padding: 10px; } .button-grid { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; } .control-panel { flex-direction: column; } }";
html += "</style>";
html += "</head>";
html += "<body>";
html += "<div class=\"container\">";
html += "<div class=\"header\">";
html += "<div class=\"time-display\" id=\"timeDisplay\">--:--:--</div>";
html += "<div class=\"date-display\" id=\"dateDisplay\">----年--月--日</div>";
html += "</div>";
html += "<div class=\"main-content\">";
html += "<div class=\"section\">";
html += "<div class=\"section-title\">空调控制</div>";
html += "<div id=\"statusMessage\" class=\"status info\" style=\"display: none;\"></div>";
html += "<div class=\"control-panel\">";
html += "<button class=\"btn btn-warning\" onclick=\"toggleRecord()\" id=\"recordBtn\">开始录入</button>";
html += "<button class=\"btn btn-secondary\" onclick=\"openSettings()\">设置</button>";
html += "</div>";
html += "<div class=\"button-grid\" id=\"buttonGrid\">";
html += "<button class=\"btn btn-primary\" onclick=\"sendIR('制冷')\">制冷</button>";
html += "<button class=\"btn btn-danger\" onclick=\"sendIR('制热')\">制热</button>";
html += "<button class=\"btn btn-success\" onclick=\"sendIR('除湿')\">除湿</button>";
html += "<button class=\"btn btn-secondary\" onclick=\"sendIR('关机')\">关机</button>";
html += "</div>";
html += "</div>";
html += "<div class=\"section\">";
html += "<div class=\"section-title\">自定义按钮</div>";
html += "<div class=\"input-group\">";
html += "<input type=\"text\" id=\"newButtonName\" placeholder=\"输入按钮名称\">";
html += "<button class=\"btn btn-success\" onclick=\"addCustomButton()\">添加按钮</button>";
html += "</div>";
html += "<div class=\"button-grid\" id=\"customButtonGrid\"></div>";
html += "</div>";
html += "</div>";
html += "</div>";
html += "<div id=\"settingsModal\" class=\"modal\">";
html += "<div class=\"modal-content\">";
html += "<span class=\"close\" onclick=\"closeSettings()\">&times;</span>";
html += "<h2 style=\"margin-bottom: 20px; color: #333;\">系统设置</h2>";
html += "<div style=\"margin-bottom: 20px;\">";
html += "<h3 style=\"color: #667eea; margin-bottom: 10px;\">时间设置</h3>";
html += "<div class=\"input-group\">";
html += "<input type=\"number\" id=\"setYear\" placeholder=\"\" min=\"2020\" max=\"2030\">";
html += "<input type=\"number\" id=\"setMonth\" placeholder=\"\" min=\"1\" max=\"12\">";
html += "<input type=\"number\" id=\"setDay\" placeholder=\"\" min=\"1\" max=\"31\">";
html += "</div>";
html += "<div class=\"input-group\">";
html += "<input type=\"number\" id=\"setHour\" placeholder=\"\" min=\"0\" max=\"23\">";
html += "<input type=\"number\" id=\"setMinute\" placeholder=\"\" min=\"0\" max=\"59\">";
html += "<input type=\"number\" id=\"setSecond\" placeholder=\"\" min=\"0\" max=\"59\">";
html += "</div>";
html += "<button class=\"btn btn-primary\" onclick=\"setDateTime()\">设置时间</button>";
html += "</div>";
html += "<div style=\"margin-bottom: 20px;\">";
html += "<h3 style=\"color: #667eea; margin-bottom: 10px;\">温度设置</h3>";
html += "<div class=\"input-group\">";
html += "<input type=\"number\" id=\"minTemp\" placeholder=\"最低温度\" min=\"5\" max=\"35\" step=\"0.5\">";
html += "<input type=\"number\" id=\"maxTemp\" placeholder=\"最高温度\" min=\"5\" max=\"35\" step=\"0.5\">";
html += "</div>";
html += "<button class=\"btn btn-primary\" onclick=\"setTemperature()\">设置温度</button>";
html += "</div>";
html += "</div>";
html += "</div>";
html += "<script>";
html += "let isRecording = false;";
html += "function updateTime() {";
html += "fetch('/api/time')";
html += ".then(response => response.json())";
html += ".then(data => {";
html += "if (data.success) {";
html += "document.getElementById('timeDisplay').textContent = String(data.hour).padStart(2, '0') + ':' + String(data.minute).padStart(2, '0') + ':' + String(data.second).padStart(2, '0');";
html += "document.getElementById('dateDisplay').textContent = data.year + '年' + data.month + '月' + data.day + '日';";
html += "}";
html += "})";
html += ".catch(error => console.error('获取时间失败:', error));";
html += "}";
html += "function showStatus(message, type) {";
html += "const statusEl = document.getElementById('statusMessage');";
html += "statusEl.textContent = message;";
html += "statusEl.className = 'status ' + type;";
html += "statusEl.style.display = 'block';";
html += "setTimeout(() => { statusEl.style.display = 'none'; }, 3000);";
html += "}";
html += "function toggleRecord() {";
html += "isRecording = !isRecording;";
html += "const btn = document.getElementById('recordBtn');";
html += "if (isRecording) {";
html += "btn.textContent = '停止录入';";
html += "btn.className = 'btn btn-danger';";
html += "showStatus('录入模式已开启,点击按钮录入红外信号', 'info');";
html += "} else {";
html += "btn.textContent = '开始录入';";
html += "btn.className = 'btn btn-warning';";
html += "showStatus('录入模式已关闭', 'info');";
html += "}";
html += "}";
html += "function sendIR(buttonName) {";
html += "if (isRecording) {";
html += "recordIR(buttonName);";
html += "} else {";
html += "fetch('/api/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ button: buttonName }) })";
html += ".then(response => response.json())";
html += ".then(data => {";
html += "if (data.success) {";
html += "showStatus('发送成功: ' + buttonName, 'success');";
html += "} else {";
html += "showStatus('发送失败: ' + (data.message || '未知错误'), 'error');";
html += "}";
html += "})";
html += ".catch(error => { showStatus('发送失败: 网络错误', 'error'); });";
html += "}";
html += "}";
html += "function recordIR(buttonName) {";
html += "showStatus('正在录入 ' + buttonName + ' 的红外信号,请对准遥控器按键...', 'info');";
html += "fetch('/api/record', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ button: buttonName }) })";
html += ".then(response => response.json())";
html += ".then(data => {";
html += "if (data.success) {";
html += "showStatus('录入成功: ' + buttonName, 'success');";
html += "} else {";
html += "showStatus('录入失败: ' + (data.message || '未知错误'), 'error');";
html += "}";
html += "})";
html += ".catch(error => { showStatus('录入失败: 网络错误', 'error'); });";
html += "}";
html += "function addCustomButton() {";
html += "const name = document.getElementById('newButtonName').value.trim();";
html += "if (!name) { showStatus('请输入按钮名称', 'error'); return; }";
html += "fetch('/api/buttons', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name }) })";
html += ".then(response => response.json())";
html += ".then(data => {";
html += "if (data.success) {";
html += "showStatus('按钮添加成功', 'success');";
html += "document.getElementById('newButtonName').value = '';";
html += "loadCustomButtons();";
html += "} else {";
html += "showStatus('添加失败: ' + (data.message || '未知错误'), 'error');";
html += "}";
html += "})";
html += ".catch(error => { showStatus('添加失败: 网络错误', 'error'); });";
html += "}";
html += "function deleteCustomButton(name) {";
html += "if (!confirm('确定要删除按钮 \"' + name + '\" 吗?')) { return; }";
html += "fetch('/api/buttons?name=' + encodeURIComponent(name), { method: 'DELETE' })";
html += ".then(response => response.json())";
html += ".then(data => {";
html += "if (data.success) {";
html += "showStatus('按钮删除成功', 'success');";
html += "loadCustomButtons();";
html += "} else {";
html += "showStatus('删除失败: ' + (data.message || '未知错误'), 'error');";
html += "}";
html += "})";
html += ".catch(error => { showStatus('删除失败: 网络错误', 'error'); });";
html += "}";
html += "function loadCustomButtons() {";
html += "fetch('/api/buttons')";
html += ".then(response => response.json())";
html += ".then(data => {";
html += "if (data.success) {";
html += "const grid = document.getElementById('customButtonGrid');";
html += "grid.innerHTML = '';";
html += "data.buttons.forEach(button => {";
html += "const btnEl = document.createElement('div');";
html += "btnEl.style.position = 'relative';";
html += "const mainBtn = document.createElement('button');";
html += "mainBtn.className = 'btn btn-primary';";
html += "mainBtn.style.width = '100%';";
html += "mainBtn.style.marginBottom = '5px';";
html += "mainBtn.textContent = button.name;";
html += "mainBtn.onclick = function() { sendIR(button.name); };";
html += "const delBtn = document.createElement('button');";
html += "delBtn.className = 'btn btn-danger';";
html += "delBtn.style.width = '100%';";
html += "delBtn.style.padding = '5px';";
html += "delBtn.style.fontSize = '12px';";
html += "delBtn.textContent = '删除';";
html += "delBtn.onclick = function() { deleteCustomButton(button.name); };";
html += "btnEl.appendChild(mainBtn);";
html += "btnEl.appendChild(delBtn);";
html += "grid.appendChild(btnEl);";
html += "});";
html += "}";
html += "})";
html += ".catch(error => console.error('加载自定义按钮失败:', error));";
html += "}";
html += "function openSettings() {";
html += "document.getElementById('settingsModal').style.display = 'block';";
html += "loadSettings();";
html += "}";
html += "function closeSettings() {";
html += "document.getElementById('settingsModal').style.display = 'none';";
html += "}";
html += "function loadSettings() {";
html += "fetch('/api/settings')";
html += ".then(response => response.json())";
html += ".then(data => {";
html += "if (data.success) {";
html += "document.getElementById('minTemp').value = data.minTemp;";
html += "document.getElementById('maxTemp').value = data.maxTemp;";
html += "}";
html += "})";
html += ".catch(error => console.error('加载设置失败:', error));";
html += "}";
html += "function setDateTime() {";
html += "const year = parseInt(document.getElementById('setYear').value);";
html += "const month = parseInt(document.getElementById('setMonth').value);";
html += "const day = parseInt(document.getElementById('setDay').value);";
html += "const hour = parseInt(document.getElementById('setHour').value);";
html += "const minute = parseInt(document.getElementById('setMinute').value);";
html += "const second = parseInt(document.getElementById('setSecond').value);";
html += "if (!year || !month || !day || isNaN(hour) || isNaN(minute) || isNaN(second)) {";
html += "showStatus('请填写完整的日期时间', 'error');";
html += "return;";
html += "}";
html += "fetch('/api/time', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ year, month, day, hour, minute, second }) })";
html += ".then(response => response.json())";
html += ".then(data => {";
html += "if (data.success) {";
html += "showStatus('时间设置成功', 'success');";
html += "updateTime();";
html += "} else {";
html += "showStatus('时间设置失败: ' + (data.message || '未知错误'), 'error');";
html += "}";
html += "})";
html += ".catch(error => { showStatus('时间设置失败: 网络错误', 'error'); });";
html += "}";
html += "function setTemperature() {";
html += "const minTemp = parseFloat(document.getElementById('minTemp').value);";
html += "const maxTemp = parseFloat(document.getElementById('maxTemp').value);";
html += "if (isNaN(minTemp) || isNaN(maxTemp)) {";
html += "showStatus('请输入有效的温度值', 'error');";
html += "return;";
html += "}";
html += "if (minTemp >= maxTemp) {";
html += "showStatus('最低温度必须小于最高温度', 'error');";
html += "return;";
html += "}";
html += "fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ minTemp, maxTemp }) })";
html += ".then(response => response.json())";
html += ".then(data => {";
html += "if (data.success) {";
html += "showStatus('温度设置成功', 'success');";
html += "} else {";
html += "showStatus('温度设置失败: ' + (data.message || '未知错误'), 'error');";
html += "}";
html += "})";
html += ".catch(error => { showStatus('温度设置失败: 网络错误', 'error'); });";
html += "}";
html += "document.addEventListener('DOMContentLoaded', function() {";
html += "updateTime();";
html += "loadCustomButtons();";
html += "setInterval(updateTime, 1000);";
html += "});";
html += "window.onclick = function(event) {";
html += "const modal = document.getElementById('settingsModal');";
html += "if (event.target == modal) {";
html += "closeSettings();";
html += "}";
html += "}";
html += "</script>";
html += "</body>";
html += "</html>";
server.send(200, "text/html", html);
}
// 获取时间
void handleGetTime() {
RtcDateTime currentTime = getRTCTime();
DynamicJsonDocument doc(200);
doc["success"] = true;
doc["year"] = currentTime.Year();
doc["month"] = currentTime.Month();
doc["day"] = currentTime.Day();
doc["hour"] = currentTime.Hour();
doc["minute"] = currentTime.Minute();
doc["second"] = currentTime.Second();
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
// 设置时间
void handleSetTime() {
if (!server.hasArg("plain")) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"缺少请求数据\"}");
return;
}
DynamicJsonDocument doc(200);
deserializeJson(doc, server.arg("plain"));
int year = doc["year"];
int month = doc["month"];
int day = doc["day"];
int hour = doc["hour"];
int minute = doc["minute"];
int second = doc["second"];
setRTCTime(year, month, day, hour, minute, second);
server.send(200, "application/json", "{\"success\":true}");
}
// 获取按钮列表
void handleGetButtons() {
DynamicJsonDocument doc(1024);
doc["success"] = true;
JsonArray buttons = doc.createNestedArray("buttons");
for (int i = 0; i < customButtonCount; i++) {
if (customButtons[i].isValid) {
JsonObject button = buttons.createNestedObject();
button["name"] = customButtons[i].name;
}
}
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
// 添加按钮
void handleAddButton() {
if (!server.hasArg("plain")) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"缺少请求数据\"}");
return;
}
DynamicJsonDocument doc(200);
deserializeJson(doc, server.arg("plain"));
String name = doc["name"];
if (name.length() == 0) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"按钮名称不能为空\"}");
return;
}
if (customButtonCount >= 10) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"自定义按钮数量已达上限\"}");
return;
}
// 检查是否已存在同名按钮
for (int i = 0; i < customButtonCount; i++) {
if (customButtons[i].isValid && customButtons[i].name == name) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"按钮名称已存在\"}");
return;
}
}
// 添加新按钮
customButtons[customButtonCount].name = name;
customButtons[customButtonCount].irData = "";
customButtons[customButtonCount].isValid = true;
customButtonCount++;
saveCustomButtons();
server.send(200, "application/json", "{\"success\":true}");
}
// 删除按钮
void handleDeleteButton() {
String name = server.arg("name");
if (name.length() == 0) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"缺少按钮名称\"}");
return;
}
// 查找并删除按钮
bool found = false;
for (int i = 0; i < customButtonCount; i++) {
if (customButtons[i].isValid && customButtons[i].name == name) {
// 删除对应的红外数据
prefs.remove(("ir_" + name).c_str());
// 移动数组元素
for (int j = i; j < customButtonCount - 1; j++) {
customButtons[j] = customButtons[j + 1];
}
customButtonCount--;
found = true;
break;
}
}
if (found) {
saveCustomButtons();
server.send(200, "application/json", "{\"success\":true}");
} else {
server.send(404, "application/json", "{\"success\":false,\"message\":\"按钮不存在\"}");
}
}
// 开始录入红外信号
void handleStartRecord() {
if (!server.hasArg("plain")) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"缺少请求数据\"}");
return;
}
DynamicJsonDocument doc(200);
deserializeJson(doc, server.arg("plain"));
String buttonName = doc["button"];
if (buttonName.length() == 0) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"按钮名称不能为空\"}");
return;
}
Serial.println("开始录入红外信号: " + buttonName);
// 等待红外信号
unsigned long startTime = millis();
while (!checkIRSignalStart() && (millis() - startTime) < 10000) {
delay(10);
}
if (millis() - startTime >= 10000) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"录入超时,未检测到红外信号\"}");
return;
}
// 接收红外信号
IRSignal signal = receiveIRSignal();
if (signal.isValid) {
// 将信号转换为字符串并保存
String irDataStr = irSignalToString(signal);
prefs.putString(("ir_" + buttonName).c_str(), irDataStr);
Serial.println("红外信号录入成功: " + buttonName);
Serial.println("mark数量: " + String(signal.markCount) + ", space数量: " + String(signal.spaceCount));
freeIRSignal(signal);
server.send(200, "application/json", "{\"success\":true}");
} else {
Serial.println("红外信号录入失败: " + buttonName);
server.send(400, "application/json", "{\"success\":false,\"message\":\"红外信号接收失败\"}");
}
}
// 发送红外信号
void handleSendIR() {
if (!server.hasArg("plain")) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"缺少请求数据\"}");
return;
}
DynamicJsonDocument doc(26700);
deserializeJson(doc, server.arg("plain"));
String buttonName = doc["button"];
if (buttonName.length() == 0) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"按钮名称不能为空\"}");
return;
}
sendStoredIRSignal(buttonName);
server.send(200, "application/json", "{\"success\":true}");
}
// 获取设置
void handleGetSettings() {
float minTemp = prefs.getFloat("min_temp", 22.0);
float maxTemp = prefs.getFloat("max_temp", 26.0);
DynamicJsonDocument doc(200);
doc["success"] = true;
doc["minTemp"] = minTemp;
doc["maxTemp"] = maxTemp;
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
// 设置温度范围
void handleSetSettings() {
if (!server.hasArg("plain")) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"缺少请求数据\"}");
return;
}
DynamicJsonDocument doc(200);
deserializeJson(doc, server.arg("plain"));
float minTemp = doc["minTemp"];
float maxTemp = doc["maxTemp"];
if (minTemp >= maxTemp) {
server.send(400, "application/json", "{\"success\":false,\"message\":\"最低温度必须小于最高温度\"}");
return;
}
prefs.putFloat("min_temp", minTemp);
prefs.putFloat("max_temp", maxTemp);
server.send(200, "application/json", "{\"success\":true}");
}
// 404处理
void handleNotFound() {
server.send(404, "text/plain", "页面未找到");
}