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