· 3 months ago · Jun 12, 2025, 05:56 PM
1/*
2 ESP32 Scheduled Motor Control with Deep Sleep - Hourly Scheduling with Configurable Run Time
3
4 DESCRIPTION:
5 This sketch creates an automated motor control system that runs a motor at user-selected
6 hours of the day with user-configurable run time while maintaining ultra-low power
7 consumption between operations.
8
9 FEATURES:
10 • Allows selection of any hours (00:00-23:00) for motor operation via web interface
11 • Configurable motor run time from 5 to 300 seconds (default: 5 seconds)
12 • Uses deep sleep between operations for maximum battery efficiency
13 • Web-based setup interface with 24 hourly checkboxes and run time slider
14 • Built-in LED status indicators during setup and operation
15 • RTC memory storage preserves settings through deep sleep cycles
16 • Automatic timeout protection (enters sleep if no configuration within 4 minutes)
17
18 HARDWARE REQUIREMENTS:
19 • ESP32 development board
20 • MOSFET (for motor control) connected to GPIO 33
21 • Motor connected through MOSFET
22 • Built-in LED on GPIO 2 (most ESP32 boards)
23 • Optional: External RTC battery for better timekeeping
24
25 SETUP PROCESS:
26 1. Upload sketch to ESP32
27 2. Connect to WiFi network "ESP32-Motor-Timer" (password: 12345678)
28 3. Navigate to the IP address shown in Serial Monitor
29 4. Sync current time from your browser
30 5. Set desired motor run time (5-300 seconds)
31 6. Check boxes for desired operation hours (e.g., 09:00, 19:00, 21:00)
32 7. ESP32 automatically enters scheduled operation mode
33
34 OPERATION:
35 • ESP32 sleeps in deep sleep mode (uses ~10µA)
36 • Wakes up at each selected hour
37 • Runs motor for configured time with LED indication
38 • Calculates next scheduled hour and returns to deep sleep
39 • Cycle repeats based on selected schedule
40
41 POWER CONSUMPTION:
42 • Deep sleep: ~10µA (months of battery life)
43 • Active operation: ~80mA for configured duration
44 • Setup mode: ~150mA (4 minute timeout)
45
46 TROUBLESHOOTING:
47 • If setup doesn't complete, ESP32 sleeps for 3 hours then restarts setup
48 • Press reset button to restart setup process
49 • Check Serial Monitor (115200 baud) for debugging information
50 • Time is automatically adjusted for Portuguese Summer Time (+1 hour)
51
52 LIBRARIES REQUIRED:
53 • ESP32Time (for RTC functionality)
54 • WiFi (built-in ESP32 library)
55
56 MIT License - Free to use and modify
57 Based on ESP32Time library examples and ESP32 deep sleep functionality
58*/
59
60#include <ESP32Time.h>
61#include <WiFi.h>
62
63#define uS_TO_S_FACTOR 1000000ULL // Conversion factor for micro seconds to seconds
64#define DEFAULT_MOTOR_ON_TIME 5 // Default motor run time in seconds
65#define MIN_MOTOR_ON_TIME 5 // Minimum motor run time in seconds
66#define MAX_MOTOR_ON_TIME 300 // Maximum motor run time in seconds
67#define MOTOR_PIN 33 // GPIO pin connected to MOSFET gate
68#define LED_PIN 32 // Built-in LED for status indication
69
70// WiFi credentials for initial time sync
71const char *ssid = "ESP32-Motor-Timer";
72const char *password = "12345678";
73
74//ESP32Time rtc;
75ESP32Time rtc(3600); // offset in seconds GMT+1 Portugal Summer Time
76WiFiServer server(80);
77
78// RTC memory variables (survive deep sleep)
79RTC_DATA_ATTR bool timeWasSet = false;
80RTC_DATA_ATTR bool scheduleWasSet = false;
81RTC_DATA_ATTR bool runTimeWasSet = false;
82RTC_DATA_ATTR int bootCount = 0;
83RTC_DATA_ATTR unsigned long webServerStartTime = 0;
84
85// Hourly schedule - 24 hour array (0=disabled, 1=enabled)
86RTC_DATA_ATTR bool hourlySchedule[24] = {0}; // All hours disabled by default
87
88// Motor run time configuration
89RTC_DATA_ATTR int motorRunTime = DEFAULT_MOTOR_ON_TIME; // Run time in seconds
90
91void setup() {
92 Serial.begin(115200);
93 delay(1000);
94
95 bootCount++;
96 Serial.println("Boot count: " + String(bootCount));
97
98 // Initialize pins
99 pinMode(MOTOR_PIN, OUTPUT);
100 pinMode(LED_PIN, OUTPUT);
101 digitalWrite(MOTOR_PIN, LOW); // Ensure motor is off initially
102
103 printWakeupReason();
104
105 // Check if this is a scheduled wake-up and all settings have been configured
106 if (timeWasSet && scheduleWasSet && runTimeWasSet && esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TIMER) {
107 handleScheduledWakeup();
108 } else {
109 // First boot, manual reset, or incomplete setup - need web interface
110 webServerStartTime = millis();
111 setupWebServer();
112 }
113}
114
115void loop() {
116 // Handle web server for time and schedule setting
117 if (!timeWasSet || !scheduleWasSet || !runTimeWasSet) {
118 // Check for 4-minute timeout
119 if (millis() - webServerStartTime > 240000) { // 4 minutes = 240,000 ms
120 Serial.println("Web server timeout - entering 3-hour sleep to save battery");
121 esp_sleep_enable_timer_wakeup(3 * 3600 * uS_TO_S_FACTOR); // 3 hours
122 esp_deep_sleep_start();
123 }
124 handleWebClient();
125 }
126}
127
128void handleScheduledWakeup() {
129 Serial.println("\n=== Scheduled Wake-up ===");
130 Serial.println("Current time: " + rtc.getTime("%A, %B %d %Y %H:%M:%S"));
131 Serial.println("Motor run time: " + String(motorRunTime) + " seconds");
132
133 // Run the motor
134 runMotor();
135
136 // Calculate next wake-up time
137 scheduleNextWakeup();
138}
139
140void runMotor() {
141 Serial.println("Starting motor for " + String(motorRunTime) + " seconds...");
142
143 // Blink LED to indicate motor operation
144 digitalWrite(LED_PIN, HIGH);
145
146 // Turn on motor via MOSFET
147 digitalWrite(MOTOR_PIN, HIGH);
148
149 // Run for specified time with status updates
150 for (int i = motorRunTime; i > 0; i--) {
151 Serial.println("Motor running... " + String(i) + "s remaining");
152 delay(1000);
153
154 // Blink LED every 5 seconds during operation (or every second if run time < 10s)
155 int blinkInterval = (motorRunTime < 10) ? 1 : 5;
156 if (i % blinkInterval == 0) {
157 digitalWrite(LED_PIN, LOW);
158 delay(100);
159 digitalWrite(LED_PIN, HIGH);
160 }
161 }
162
163 // Turn off motor
164 digitalWrite(MOTOR_PIN, LOW);
165 digitalWrite(LED_PIN, LOW);
166
167 Serial.println("Motor stopped.");
168}
169
170void scheduleNextWakeup() {
171 struct tm timeinfo = rtc.getTimeStruct();
172 int currentHour = timeinfo.tm_hour;
173 int currentMinute = timeinfo.tm_min;
174
175 // Calculate seconds until next scheduled hour
176 unsigned long sleepTime = calculateSleepTime(currentHour, currentMinute);
177
178 if (sleepTime > 0) {
179 Serial.println("Next wake-up in " + String(sleepTime / 3600) + " hours and " +
180 String((sleepTime % 3600) / 60) + " minutes");
181
182 // Configure and enter deep sleep
183 esp_sleep_enable_timer_wakeup(sleepTime * uS_TO_S_FACTOR);
184 Serial.println("Entering deep sleep...");
185 Serial.flush();
186 esp_deep_sleep_start();
187 } else {
188 Serial.println("No scheduled hours found - entering 24-hour sleep");
189 esp_sleep_enable_timer_wakeup(24 * 3600 * uS_TO_S_FACTOR); // 24 hours
190 esp_deep_sleep_start();
191 }
192}
193
194unsigned long calculateSleepTime(int currentHour, int currentMinute) {
195 // Find next scheduled hour
196 int nextHour = -1;
197
198 // First, check if there are any hours scheduled later today
199 for (int h = currentHour + 1; h < 24; h++) {
200 if (hourlySchedule[h]) {
201 nextHour = h;
202 break;
203 }
204 }
205
206 // If no hours found later today, check from beginning of next day
207 if (nextHour == -1) {
208 for (int h = 0; h < 24; h++) {
209 if (hourlySchedule[h]) {
210 nextHour = h + 24; // Add 24 to indicate next day
211 break;
212 }
213 }
214 }
215
216 // If still no hours found, return 0 (no schedule set)
217 if (nextHour == -1) {
218 return 0;
219 }
220
221 // Calculate sleep time
222 int currentTotalMinutes = currentHour * 60 + currentMinute;
223 int nextTotalMinutes = (nextHour % 24) * 60; // Target minute is always 0
224
225 // If next hour is tomorrow, add 24 hours worth of minutes
226 if (nextHour >= 24) {
227 nextTotalMinutes += 24 * 60;
228 }
229
230 int sleepMinutes = nextTotalMinutes - currentTotalMinutes;
231 return sleepMinutes * 60; // Convert to seconds
232}
233
234void setupWebServer() {
235 Serial.println("\n=== Setting up Web Server for Configuration ===");
236 Serial.println("Connect to WiFi network: " + String(ssid));
237 Serial.println("Password: " + String(password));
238 Serial.println("TIMEOUT: 4 minutes (will sleep for 3 hours if no configuration)");
239
240 WiFi.softAP(ssid, password);
241 IPAddress IP = WiFi.softAPIP();
242 Serial.println("Web interface: http://" + IP.toString());
243 server.begin();
244
245 // Blink LED to indicate setup mode
246 for (int i = 0; i < 10; i++) {
247 digitalWrite(LED_PIN, HIGH);
248 delay(200);
249 digitalWrite(LED_PIN, LOW);
250 delay(200);
251 }
252}
253
254void handleWebClient() {
255 WiFiClient client = server.available();
256
257 if (client) {
258 Serial.println("Client connected");
259 String currentLine = "";
260
261 while (client.connected()) {
262 if (client.available()) {
263 char c = client.read();
264 if (c == '\n') {
265 if (currentLine.length() == 0) {
266 sendWebPage(client);
267 break;
268 } else {
269 currentLine = "";
270 }
271 } else if (c != '\r') {
272 currentLine += c;
273 }
274
275 if (currentLine.endsWith("POST /syncTime")) {
276 handleTimeSyncRequest(client);
277 } else if (currentLine.endsWith("POST /setRunTime")) {
278 handleRunTimeRequest(client);
279 } else if (currentLine.endsWith("POST /setSchedule")) {
280 handleScheduleRequest(client);
281 }
282 }
283 }
284 client.stop();
285 }
286}
287
288void sendWebPage(WiFiClient &client) {
289 // Calculate remaining time
290 unsigned long elapsed = millis() - webServerStartTime;
291 unsigned long remaining = (240000 - elapsed) / 1000; // seconds remaining
292
293 client.println("HTTP/1.1 200 OK");
294 client.println("Content-type:text/html");
295 client.println();
296 client.println("<!DOCTYPE html><html>");
297 client.println("<head><title>ESP32 Motor Timer Setup</title>");
298 client.println("<meta name='viewport' content='width=device-width, initial-scale=1'>");
299 client.println("<style>");
300 client.println("body{font-family:Arial;text-align:center;padding:20px;background:#f0f0f0;}");
301 client.println(".container{max-width:600px;margin:0 auto;background:white;padding:30px;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,0.1);}");
302 client.println("input,button{padding:10px;margin:5px;font-size:16px;border:1px solid #ddd;border-radius:5px;}");
303 client.println("button{background:#4CAF50;color:white;border:none;cursor:pointer;padding:15px 30px;}");
304 client.println("button:hover{background:#45a049;}");
305 client.println(".timeout{color:red;font-weight:bold;}");
306 client.println(".step{margin:20px 0;padding:15px;background:#f9f9f9;border-radius:5px;}");
307 client.println(".schedule-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:20px 0;}");
308 client.println(".hour-checkbox{display:flex;align-items:center;padding:8px;background:white;border:1px solid #ddd;border-radius:5px;}");
309 client.println(".hour-checkbox input{margin-right:8px;}");
310 client.println(".hour-checkbox:hover{background:#f0f8ff;}");
311 client.println(".selected-hours{background:#e8f5e8;padding:10px;border-radius:5px;margin:10px 0;}");
312 client.println(".runtime-control{display:flex;align-items:center;justify-content:center;gap:15px;margin:15px 0;}");
313 client.println(".runtime-slider{width:200px;}");
314 client.println(".runtime-display{font-weight:bold;color:#2196F3;min-width:80px;}");
315 client.println("</style></head>");
316 client.println("<body>");
317 client.println("<div class='container'>");
318 client.println("<h1>ESP32 Motor Timer Setup</h1>");
319 client.println("<div class='timeout'>Timeout: <span id='countdown'>" + String(remaining) + "</span> seconds</div>");
320
321 // Step 1: Time sync
322 client.println("<div class='step'>");
323 client.println("<h3>Step 1: Set Current Time</h3>");
324 if (timeWasSet) {
325 client.println("<p>✅ Time set: " + rtc.getTime("%H:%M:%S %d/%m/%Y") + "</p>");
326 } else {
327 client.println("<h4 id='currentTime'></h4>");
328 client.println("<form action='/syncTime' method='POST'>");
329 client.println("<input type='hidden' name='epochTime' id='hiddenEpochTime'>");
330 client.println("<button type='submit'>Sync Time</button>");
331 client.println("</form>");
332 }
333 client.println("</div>");
334
335 // Step 2: Motor run time setup
336 client.println("<div class='step'>");
337 client.println("<h3>Step 2: Set Motor Run Time</h3>");
338 if (runTimeWasSet) {
339 client.println("<div class='selected-hours'>");
340 client.println("<p>✅ Run time set: " + String(motorRunTime) + " seconds</p>");
341 client.println("</div>");
342 } else {
343 client.println("<p>Set how long the motor should run each time (5-300 seconds):</p>");
344 client.println("<form action='/setRunTime' method='POST'>");
345 client.println("<div class='runtime-control'>");
346 client.println("<label>5s</label>");
347 client.println("<input type='range' name='runTime' id='runTimeSlider' class='runtime-slider' ");
348 client.println("min='" + String(MIN_MOTOR_ON_TIME) + "' max='" + String(MAX_MOTOR_ON_TIME) + "' ");
349 client.println("value='" + String(motorRunTime) + "' oninput='updateRunTimeDisplay(this.value)'>");
350 client.println("<label>300s</label>");
351 client.println("</div>");
352 client.println("<div class='runtime-display' id='runTimeDisplay'>" + String(motorRunTime) + " seconds</div>");
353 client.println("<button type='submit'>Set Run Time</button>");
354 client.println("</form>");
355 }
356 client.println("</div>");
357
358 // Step 3: Schedule setup
359 client.println("<div class='step'>");
360 client.println("<h3>Step 3: Select Operation Hours</h3>");
361 client.println("<p>Check the boxes for hours when the motor should run:</p>");
362
363 if (scheduleWasSet) {
364 client.println("<div class='selected-hours'>");
365 client.println("<p>✅ Schedule set! Motor will run at:</p>");
366 String selectedHours = "";
367 int count = 0;
368 for (int h = 0; h < 24; h++) {
369 if (hourlySchedule[h]) {
370 if (count > 0) selectedHours += ", ";
371 selectedHours += String(h < 10 ? "0" : "") + String(h) + ":00";
372 count++;
373 }
374 }
375 if (count == 0) {
376 client.println("<p>No hours selected</p>");
377 } else {
378 client.println("<p>" + selectedHours + "</p>");
379 client.println("<p>Total: " + String(count) + " times per day</p>");
380 if (runTimeWasSet) {
381 client.println("<p>Each run: " + String(motorRunTime) + " seconds</p>");
382 }
383 }
384 client.println("</div>");
385 } else {
386 client.println("<form action='/setSchedule' method='POST'>");
387 client.println("<div class='schedule-grid'>");
388
389 // Create 24 checkboxes for each hour
390 for (int h = 0; h < 24; h++) {
391 client.println("<div class='hour-checkbox'>");
392 client.println("<input type='checkbox' name='hour" + String(h) + "' value='1'" +
393 (hourlySchedule[h] ? " checked" : "") + ">");
394 client.println("<label>" + String(h < 10 ? "0" : "") + String(h) + ":00</label>");
395 client.println("</div>");
396 }
397
398 client.println("</div>");
399 client.println("<button type='submit'>Set Schedule</button>");
400 client.println("</form>");
401 }
402 client.println("</div>");
403
404 if (timeWasSet && scheduleWasSet && runTimeWasSet) {
405 client.println("<div class='step'>");
406 client.println("<h3>✅ Setup Complete!</h3>");
407 client.println("<p>ESP32 will now enter scheduled operation mode.</p>");
408 client.println("<p><strong>Configuration Summary:</strong></p>");
409 client.println("<ul style='text-align:left;display:inline-block;'>");
410 client.println("<li>Motor run time: " + String(motorRunTime) + " seconds</li>");
411
412 String selectedHours = "";
413 int count = 0;
414 for (int h = 0; h < 24; h++) {
415 if (hourlySchedule[h]) {
416 if (count > 0) selectedHours += ", ";
417 selectedHours += String(h < 10 ? "0" : "") + String(h) + ":00";
418 count++;
419 }
420 }
421 client.println("<li>Schedule: " + selectedHours + "</li>");
422 client.println("<li>Total daily runtime: " + String(count * motorRunTime) + " seconds</li>");
423 client.println("</ul>");
424 client.println("<button onclick='startOperation()'>Start Operation</button>");
425 client.println("</div>");
426 }
427
428 client.println("</div>");
429
430 // JavaScript for time updates, countdown, and run time display
431 client.println("<script>");
432 client.println("var countdown = " + String(remaining) + ";");
433 client.println("function updateTime(){");
434 client.println("var now=new Date();");
435 client.println("if(document.getElementById('currentTime'))");
436 client.println("document.getElementById('currentTime').innerHTML='Current Time: '+now.toLocaleString();");
437 client.println("if(document.getElementById('hiddenEpochTime'))");
438 client.println("document.getElementById('hiddenEpochTime').value=Math.floor(now.getTime()/1000);");
439 client.println("}");
440 client.println("function updateCountdown(){");
441 client.println("countdown--;");
442 client.println("document.getElementById('countdown').innerHTML=countdown;");
443 client.println("if(countdown<=0){");
444 client.println("document.body.innerHTML='<h2>Timeout reached - ESP32 entering sleep mode</h2>';");
445 client.println("}");
446 client.println("}");
447 client.println("function updateRunTimeDisplay(value){");
448 client.println("document.getElementById('runTimeDisplay').innerHTML=value+' seconds';");
449 client.println("}");
450 client.println("function startOperation(){");
451 client.println("document.body.innerHTML='<h2>Starting scheduled operation...</h2><p>ESP32 entering deep sleep mode</p>';");
452 client.println("setTimeout(function(){window.location.reload();},2000);");
453 client.println("}");
454 client.println("setInterval(updateTime,1000);");
455 client.println("setInterval(updateCountdown,1000);");
456 client.println("updateTime();");
457 client.println("</script></body></html>");
458 client.println();
459}
460
461void handleTimeSyncRequest(WiFiClient &client) {
462 String requestBody = "";
463 while (client.available()) {
464 requestBody += (char)client.read();
465 }
466
467 int epochIndex = requestBody.indexOf("epochTime=");
468 if (epochIndex != -1) {
469 long epochTime = requestBody.substring(epochIndex + 10).toInt();
470 rtc.setTime(epochTime);
471 timeWasSet = true;
472
473 Serial.println("Time synchronized: " + rtc.getTime("%A, %B %d %Y %H:%M:%S"));
474
475 // Send redirect back to main page
476 client.println("HTTP/1.1 302 Found");
477 client.println("Location: /");
478 client.println();
479 }
480}
481
482void handleRunTimeRequest(WiFiClient &client) {
483 String requestBody = "";
484 while (client.available()) {
485 requestBody += (char)client.read();
486 }
487
488 Serial.println("Run time request body: " + requestBody);
489
490 int runTimeIndex = requestBody.indexOf("runTime=");
491 if (runTimeIndex != -1) {
492 int newRunTime = requestBody.substring(runTimeIndex + 8).toInt();
493
494 // Validate run time is within acceptable range
495 if (newRunTime >= MIN_MOTOR_ON_TIME && newRunTime <= MAX_MOTOR_ON_TIME) {
496 motorRunTime = newRunTime;
497 runTimeWasSet = true;
498 Serial.println("Motor run time set to: " + String(motorRunTime) + " seconds");
499 } else {
500 Serial.println("Invalid run time: " + String(newRunTime) + ". Using default: " + String(DEFAULT_MOTOR_ON_TIME));
501 motorRunTime = DEFAULT_MOTOR_ON_TIME;
502 runTimeWasSet = true;
503 }
504 }
505
506 // Send redirect back to main page
507 client.println("HTTP/1.1 302 Found");
508 client.println("Location: /");
509 client.println();
510}
511
512void handleScheduleRequest(WiFiClient &client) {
513 String requestBody = "";
514 while (client.available()) {
515 requestBody += (char)client.read();
516 }
517
518 Serial.println("Schedule request body: " + requestBody);
519
520 // Reset all hours first
521 for (int h = 0; h < 24; h++) {
522 hourlySchedule[h] = false;
523 }
524
525 // Parse checkbox data
526 for (int h = 0; h < 24; h++) {
527 String hourParam = "hour" + String(h) + "=1";
528 if (requestBody.indexOf(hourParam) != -1) {
529 hourlySchedule[h] = true;
530 Serial.println("Hour " + String(h) + " enabled");
531 }
532 }
533
534 scheduleWasSet = true;
535
536 Serial.println("Schedule updated:");
537 int enabledCount = 0;
538 for (int h = 0; h < 24; h++) {
539 if (hourlySchedule[h]) {
540 Serial.println("- " + String(h) + ":00");
541 enabledCount++;
542 }
543 }
544 Serial.println("Total enabled hours: " + String(enabledCount));
545 Serial.println("Total daily motor runtime: " + String(enabledCount * motorRunTime) + " seconds");
546
547 // If all settings are configured, start operation
548 if (timeWasSet && scheduleWasSet && runTimeWasSet) {
549 delay(2000); // Allow user to see confirmation
550 scheduleNextWakeup();
551 }
552
553 // Send redirect back to main page
554 client.println("HTTP/1.1 302 Found");
555 client.println("Location: /");
556 client.println();
557}
558
559void printWakeupReason() {
560 esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
561
562 switch (wakeup_reason) {
563 case ESP_SLEEP_WAKEUP_TIMER:
564 Serial.println("Wake-up: Scheduled timer");
565 break;
566 case ESP_SLEEP_WAKEUP_EXT0:
567 Serial.println("Wake-up: External signal RTC_IO");
568 break;
569 case ESP_SLEEP_WAKEUP_EXT1:
570 Serial.println("Wake-up: External signal RTC_CNTL");
571 break;
572 default:
573 Serial.println("Wake-up: Power on or reset");
574 break;
575 }
576}