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

827 lines
30 KiB
C++

//使用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.data = nullptr;
signal.length = 0;
signal.isValid = false;
if (dataStr.length() == 0) {
return signal;
}
// 计算数据长度
int commaCount = 0;
for (int i = 0; i < dataStr.length(); i++) {
if (dataStr[i] == ',') commaCount++;
}
signal.length = commaCount + 1;
// 分配内存
signal.data = (unsigned int*)malloc(signal.length * sizeof(unsigned int));
if (signal.data == 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);
signal.data[index] = valueStr.toInt();
index++;
startPos = i + 1;
}
}
signal.isValid = true;
return signal;
}
// 将红外信号转换为字符串
String irSignalToString(const IRSignal& signal) {
if (!signal.isValid || signal.data == nullptr || signal.length == 0) {
return "";
}
String result = "";
for (int i = 0; i < signal.length; i++) {
if (i > 0) result += ",";
result += String(signal.data[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("信号长度: " + String(signal.length));
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(200);
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", "页面未找到");
}