#include
#include
#include
#include
#include
// WiFi配置
const char* ssid = “wrTi”;
const char* password = “chenzeqi”;
ESP8266WebServer server(80);
Servo myservo;
const int servoPin = D1; // 舵机信号线连接到D1 (GPIO5)
int currentServoAngle = 0; // 当前舵机角度
// NTP 时间配置(中国时区,东八区)
const char* ntpServer = “pool.ntp.org”;
const long gmtOffset_sec = 8 * 3600;
const int daylightOffset_sec = 0;
// 最大支持闹钟数量
#define MAX_ALARMS 5
struct Alarm {
int hour = 0;
int minute = 0;
int second = 0; // 新增秒字段
int startAngle = 0;
int targetAngle = 0;
bool active = false;
bool triggeredToday = false;
};
Alarm alarms[MAX_ALARMS];
// 全局变量存储脉冲范围 – 扩展为400-4000微秒
int servoMinPulse = 400; // 0度对应的最小脉宽
int servoMaxPulse = 4000; // 360度对应的最大脉宽
// EEPROM相关定义
const int EEPROM_SIZE = 512;
const int EEPROM_START_ADDR = 0;
const int EEPROM_ANGLE_ADDR = 100; // 舵机角度存储地址
// 网页HTML,添加秒选择功能
const char webpage[] PROGMEM = R”=====(
智能浇水舵机实时控制
当前系统时间: –:–:–
手动控制舵机角度
舵机测试
脉冲宽度微调
设置浇水闹钟 (最多5个)
当前闹钟列表
)=====”;
// 辅助函数,把0~360度映射到微秒范围
int angleToPulse(int angle) {
angle = constrain(angle, 0, 360);
// 直接将0-360度线性映射到servoMinPulse-servoMaxPulse微秒
long pulse = map(angle, 0, 360, servoMinPulse, servoMaxPulse);
// 确保脉冲在安全范围内
pulse = constrain(pulse, servoMinPulse, servoMaxPulse);
return pulse;
}
// 从EEPROM读取舵机角度
int loadAngleFromEEPROM() {
EEPROM.begin(EEPROM_SIZE);
int angle = EEPROM.read(EEPROM_ANGLE_ADDR);
EEPROM.end();
// 验证角度值是否有效
if (angle < 0 || angle > 360) {
angle = 0; // 默认值
}
return angle;
}
// 保存舵机角度到EEPROM
void saveAngleToEEPROM(int angle) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.write(EEPROM_ANGLE_ADDR, angle);
EEPROM.commit();
EEPROM.end();
}
// 从EEPROM读取脉冲范围
void loadPulseRangeFromEEPROM() {
EEPROM.begin(EEPROM_SIZE);
int minPulse = EEPROM.read(EEPROM_ANGLE_ADDR + 1);
int maxPulse = EEPROM.read(EEPROM_ANGLE_ADDR + 2);
// 如果读取的值无效,使用默认值
if (minPulse >= 200 && minPulse <= 2000) {
servoMinPulse = minPulse;
}
if (maxPulse >= 2000 && maxPulse <= 5000) {
servoMaxPulse = maxPulse;
}
EEPROM.end();
}
// 保存脉冲范围到EEPROM
void savePulseRangeToEEPROM() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.write(EEPROM_ANGLE_ADDR + 1, servoMinPulse);
EEPROM.write(EEPROM_ANGLE_ADDR + 2, servoMaxPulse);
EEPROM.commit();
EEPROM.end();
}
// EEPROM读取闹钟数据
void loadAlarmsFromEEPROM() {
EEPROM.begin(EEPROM_SIZE);
for (int i = 0; i < MAX_ALARMS; i++) {
int baseAddr = EEPROM_START_ADDR + i * sizeof(Alarm);
alarms[i].hour = EEPROM.read(baseAddr);
alarms[i].minute = EEPROM.read(baseAddr + 1);
alarms[i].second = EEPROM.read(baseAddr + 2); // 读取秒
alarms[i].startAngle = EEPROM.read(baseAddr + 3);
alarms[i].targetAngle = EEPROM.read(baseAddr + 4);
alarms[i].active = EEPROM.read(baseAddr + 5);
alarms[i].triggeredToday = false;
if (alarms[i].hour > 23 || alarms[i].minute > 59 || alarms[i].second > 59 ||
alarms[i].startAngle > 360 || alarms[i].targetAngle > 360 ||
(alarms[i].active != 0 && alarms[i].active != 1)) {
alarms[i].active = false;
}
}
EEPROM.end();
}
// EEPROM写入闹钟数据
void saveAlarmsToEEPROM() {
EEPROM.begin(EEPROM_SIZE);
for (int i = 0; i < MAX_ALARMS; i++) {
int baseAddr = EEPROM_START_ADDR + i * sizeof(Alarm);
EEPROM.write(baseAddr, alarms[i].hour);
EEPROM.write(baseAddr + 1, alarms[i].minute);
EEPROM.write(baseAddr + 2, alarms[i].second); // 保存秒
EEPROM.write(baseAddr + 3, alarms[i].startAngle);
EEPROM.write(baseAddr + 4, alarms[i].targetAngle);
EEPROM.write(baseAddr + 5, alarms[i].active ? 1 : 0);
}
EEPROM.commit();
EEPROM.end();
}
void setupTime() {
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.print("等待时间同步");
time_t now = time(nullptr);
int timeout = 0;
while (now < 8 * 3600 * 2 && timeout < 20) {
delay(500);
Serial.print(".");
now = time(nullptr);
timeout++;
}
Serial.println();
}
bool myGetLocalTime(struct tm * info) {
time_t t = time(nullptr);
if (t == 0) return false;
struct tm *timeinfo = localtime(&t);
if (timeinfo == nullptr) return false;
*info = *timeinfo;
return true;
}
void notFound() {
server.send(404, "text/plain", "Not Found");
}
void setup() {
Serial.begin(115200);
delay(1000); // 给串口时间初始化
// 先加载EEPROM数据
loadAlarmsFromEEPROM();
loadPulseRangeFromEEPROM();
// 从EEPROM读取上次保存的舵机角度
currentServoAngle = loadAngleFromEEPROM();
Serial.printf("从EEPROM读取舵机角度: %d\n", currentServoAngle);
Serial.printf("从EEPROM读取脉冲范围: %d - %d 微秒\n", servoMinPulse, servoMaxPulse);
// 初始化舵机 - 关键修改:避免默认的90度位置
Serial.println("初始化舵机...");
// 重要:在连接舵机之前,先设置引脚模式和初始脉冲
pinMode(servoPin, OUTPUT);
// 直接发送目标脉冲,避免舵机库的默认行为
int pulseWidth = angleToPulse(currentServoAngle);
digitalWrite(servoPin, HIGH);
delayMicroseconds(pulseWidth);
digitalWrite(servoPin, LOW);
delay(20); // 等待一个PWM周期
// 现在连接舵机,但使用我们预定义的脉冲范围
bool attached = myservo.attach(servoPin, servoMinPulse, servoMaxPulse);
if(attached) {
Serial.println("舵机连接成功");
} else {
Serial.println("舵机连接失败");
}
// 立即设置舵机角度,确保不会转到默认位置
myservo.writeMicroseconds(pulseWidth);
Serial.printf("初始化舵机角度为: %d度 (脉宽: %d微秒)\n", currentServoAngle, pulseWidth);
// 延迟一段时间让舵机到达指定位置
delay(1000);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("正在连接WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi已连接!");
Serial.print("IP地址为: ");
Serial.println(WiFi.localIP());
setupTime();
// 网页主页
server.on("/", [](){
server.send_P(200, "text/html; charset=utf-8", webpage);
});
// 设置舵机角度,支持0-360度
server.on("/setAngle", [](){
if (server.hasArg("value")) {
int angle = server.arg("value").toInt();
if (angle >= 0 && angle <= 360) {
currentServoAngle = angle;
int pulseWidth = angleToPulse(currentServoAngle);
// 先分离再重新连接舵机,确保脉冲范围正确
if(myservo.attached()) {
myservo.detach();
delay(50);
}
myservo.attach(servoPin, servoMinPulse, servoMaxPulse);
delay(50);
myservo.writeMicroseconds(pulseWidth);
// 保存角度到EEPROM
saveAngleToEEPROM(currentServoAngle);
Serial.printf("舵机角度设为: %d度 (脉宽: %d微秒)\n", currentServoAngle, pulseWidth);
// 保持连接一小段时间确保舵机接收到信号
delay(300);
server.send(200, "text/plain", "OK");
return;
}
}
server.send(400, "text/plain", "非法参数");
});
// 获取当前舵机角度
server.on("/getCurrentAngle", [](){
String json = "{";
json += "\"angle\":" + String(currentServoAngle);
json += "}";
server.send(200, "application/json", json);
});
// 获取当前脉冲范围
server.on("/getPulseRange", [](){
String json = "{";
json += "\"minPulse\":" + String(servoMinPulse) + ",";
json += "\"maxPulse\":" + String(servoMaxPulse);
json += "}";
server.send(200, "application/json", json);
});
// 设置脉冲范围
server.on("/setPulseRange", [](){
if (server.hasArg("min") && server.hasArg("max")) {
int minPulse = server.arg("min").toInt();
int maxPulse = server.arg("max").toInt();
// 扩展参数检查范围
if (minPulse >= 200 && minPulse <= 2000 &&
maxPulse >= 2000 && maxPulse <= 5000 &&
minPulse < maxPulse) {
servoMinPulse = minPulse;
servoMaxPulse = maxPulse;
// 保存脉冲范围到EEPROM
savePulseRangeToEEPROM();
// 重新连接舵机使用新的脉冲范围
if(myservo.attached()) {
myservo.detach();
delay(50);
}
myservo.attach(servoPin, servoMinPulse, servoMaxPulse);
delay(50);
// 使用新的脉冲范围重新设置当前角度
int pulseWidth = angleToPulse(currentServoAngle);
myservo.writeMicroseconds(pulseWidth);
Serial.printf("脉冲范围更新为: %d - %d 微秒\n", servoMinPulse, servoMaxPulse);
Serial.printf("当前角度 %d度 对应脉宽: %d微秒\n", currentServoAngle, pulseWidth);
server.send(200, "text/plain", "脉冲范围已更新");
return;
}
}
server.send(400, "text/plain", "非法脉冲范围");
});
// 添加闹钟,支持秒
server.on("/addAlarm", [](){
if (server.hasArg("hour") && server.hasArg("minute") && server.hasArg("second") &&
server.hasArg("start") && server.hasArg("target")) {
int hour = server.arg("hour").toInt();
int minute = server.arg("minute").toInt();
int second = server.arg("second").toInt();
int startAngle = server.arg("start").toInt();
int targetAngle = server.arg("target").toInt();
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 ||
startAngle < 0 || startAngle > 360 || targetAngle < 0 || targetAngle > 360) {
server.send(400, “text/plain”, “参数超出范围”);
return;
}
for (int i = 0; i < MAX_ALARMS; i++) {
if (!alarms[i].active) {
alarms[i].hour = hour;
alarms[i].minute = minute;
alarms[i].second = second;
alarms[i].startAngle = startAngle;
alarms[i].targetAngle = targetAngle;
alarms[i].active = true;
alarms[i].triggeredToday = false;
Serial.printf("新增闹钟: %02d:%02d:%02d 从%d°转到%d°\n", hour, minute, second, startAngle, targetAngle);
saveAlarmsToEEPROM();
server.send(200, "text/plain", "闹钟添加成功");
return;
}
}
server.send(200, "text/plain", "闹钟数量已满");
} else {
server.send(400, "text/plain", "参数缺失");
}
});
// 获取闹钟列表
server.on("/getAlarms", [](){
String json = "[";
bool first = true;
for (int i = 0; i < MAX_ALARMS; i++) {
if (alarms[i].active) {
if (!first) {
json += ",";
} else {
first = false;
}
json += "{";
json += "\"hour\":" + String(alarms[i].hour) + ",";
json += "\"minute\":" + String(alarms[i].minute) + ",";
json += "\"second\":" + String(alarms[i].second) + ",";
json += "\"startAngle\":" + String(alarms[i].startAngle) + ",";
json += "\"targetAngle\":" + String(alarms[i].targetAngle);
json += "}";
}
}
json += "]";
server.send(200, "application/json", json);
});
// 删除闹钟
server.on("/deleteAlarm", []() {
if (server.hasArg("index")) {
int idx = server.arg("index").toInt();
int activeCount = 0;
for(int i=0; i
server.send(400, “text/plain”, “索引超出范围”);
return;
}
int curIndex = 0;
for(int i=0; i