887 lines
32 KiB
C++
887 lines
32 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.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()\">×</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);
|
||
|
||
Serial.printf("温度设置已更新 - 最低: %.1f°C, 最高: %.1f°C\n", minTemp, maxTemp);
|
||
|
||
// 温度设置更改后立即执行一次判断
|
||
Serial.println("温度设置已更改,立即执行核心判断...");
|
||
executeJudgeLogic();
|
||
|
||
server.send(200, "application/json", "{\"success\":true}");
|
||
}
|
||
|
||
// 404处理
|
||
void handleNotFound() {
|
||
server.send(404, "text/plain", "页面未找到");
|
||
} |