Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*
- ========================================================================================
- Autonomous & Resilient Watering System for ESP32
- ========================================================================================
- -- DESCRIPTION --
- This sketch turns an ESP32 into a highly power-efficient and reliable automated
- watering system. It is designed to be "set and forget." After an initial web-based
- setup, it will run its schedule autonomously, with or without a WiFi connection.
- All settings are saved to the ESP32's internal flash memory, making the system
- resilient to power cuts.
- -- KEY FEATURES --
- - Power-Efficient: Uses deep sleep to run for weeks or months on battery power.
- - Web-Based Setup: Creates a WiFi hotspot to configure the schedule from your phone.
- - Autonomous Offline Operation: Continues to run its schedule even if WiFi is down.
- - Power-Cut Proof: Reloads saved settings from flash memory after a power failure.
- - Smart Time Sync: Corrects clock drift with an NTP server when online.
- - Telegram Notifications: Sends status messages when online.
- - PUSHBUTTON RESET: A dedicated button can be pressed to wipe the settings and
- force the device back into Configuration Mode without needing to re-upload code.
- ========================================================================================
- HOW TO USE
- ========================================================================================
- -- STEP 1: USER CONFIGURATION --
- Before uploading, you MUST fill in the following credentials.
- 1. WiFi Network:
- - const char* network = "YOUR_WIFI_NAME";
- - const char* pass = "YOUR_WIFI_PASSWORD";
- 2. Telegram Bot:
- - const char* token = "YOUR_TELEGRAM_BOT_TOKEN";
- - int64_t userid = YOUR_TELEGRAM_USER_ID;
- 3. Timezone:
- - #define MYTZ "YOUR_TIMEZONE_STRING" (e.g., "CET-1CEST,M3.5.0,M10.5.0/3")
- Find yours at: https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
- -- STEP 2: HARDWARE SETUP --
- 1. Motor Driver: Connect to the pin defined by 'enableMotorPin' (default: GPIO 10).
- 2. Status LED: (Optional) Connect to the pin defined by 'ledPin' (default: GPIO 8).
- 3. Reset Button (IMPORTANT):
- - Connect a momentary pushbutton between GPIO 4 and 3.3V.
- - For stability, connect a 10k Ohm pull-down resistor between GPIO 4 and GND.
- This prevents the pin from "floating" and causing accidental resets.
- -- STEP 3: FIRST-TIME SETUP (CONFIGURATION PORTAL) --
- 1. Upload this code to your ESP32 and open the Serial Monitor.
- 2. The ESP32 will create a WiFi network named "WateringSystem_Setup".
- 3. Connect to this network with your phone. A configuration page should open automatically.
- 4. Set the schedule and motor run time, then click "Save Settings & Sync Time".
- 5. The ESP32 will save your settings, go to sleep, and begin its schedule.
- -- STEP 4: RE-CONFIGURING THE DEVICE --
- If you ever need to change the schedule or settings:
- 1. Press and hold the reset button (on GPIO 4) for about 2 seconds.
- 2. Release the button.
- 3. The device will wake up, wipe its old settings, and start the "WateringSystem_Setup"
- WiFi network again.
- 4. Follow the steps from "FIRST-TIME SETUP" to re-configure it.
- */
- // =====================================================================================
- // FIRMWARE VERSION
- // =====================================================================================
- #define FW_VERSION "3.3"
- #define FW_FEATURES "C3, Window, Reset, VerID"
- // =====================================================================================
- // --- Required Libraries ---
- #include <WiFi.h>
- #include <WiFiClientSecure.h>
- #include <AsyncTelegram2.h>
- #include "time.h"
- #include <WebServer.h>
- #include <DNSServer.h>
- #include <Preferences.h>
- // --- Credentials and Configuration ---
- const char* network = "YOUR_WIFI_NAME";
- const char* pass = "YOUR_WIFI_PASSWORD";
- const char* token = "YOUR_TELEGRAM_BOT_TOKEN";
- int64_t userid = YOUR_TELEGRAM_USER_ID;
- #define MYTZ "WET0WEST,M3.5.0/1,M10.5.0/2"
- #define uS_TO_S_FACTOR 1000000ULL
- const unsigned long PORTAL_TIMEOUT_MS = 240000;
- const int FALLBACK_SLEEP_SECONDS = 3600;
- // --- RTC Memory ---
- RTC_DATA_ATTR int bootCount = 0;
- // --- Global Configuration Variables ---
- uint32_t wateringSchedule = 0;
- int motorRunSeconds = 10;
- // --- Hardware Pins ---
- // These pins are valid on most ESP32-C3 SuperMini boards.
- const int ledPin = 8;
- const int enableMotorPin = 10;
- const int resetButtonPin = 4; // GPIO4 is an RTC pin, valid for C3 wakeup.
- // --- Core Objects ---
- WiFiClientSecure client; AsyncTelegram2 myBot(client);
- DNSServer dnsServer; WebServer server(80);
- Preferences preferences; bool portalSucceeded = false;
- // --- Function Declarations ---
- void runMotor(); void blinkLED(int numBlinks, int blinkInterval);
- bool connectToWiFi(); void syncTimeAndSetupBot();
- void sendTelegramMessage(const char* msg); void startConfigurationPortal();
- void handleRoot(); void handleSave(); void handleNotFound();
- void setup() {
- Serial.begin(115200);
- delay(1000);
- // --- NEW: Print Firmware Version Banner ---
- Serial.println("\n\n================================================");
- Serial.printf(" Watering System FW Version: %s\n", FW_VERSION);
- Serial.printf(" Features: %s\n", FW_FEATURES);
- Serial.println("================================================");
- pinMode(ledPin, OUTPUT); pinMode(enableMotorPin, OUTPUT);
- digitalWrite(ledPin, LOW); // SUPERMINI led is ON when LOW
- // --- Check Wakeup Reason ---
- esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
- // C3 CHANGE: Check for the generic GPIO wakeup cause instead of the specific EXT0.
- if (wakeup_reason == ESP_SLEEP_WAKEUP_GPIO) {
- Serial.println("\n\nWakeup caused by external signal on GPIO (Reset Button).");
- Serial.println("Wiping configuration and starting portal...");
- preferences.begin("water_cfg", false);
- preferences.clear();
- preferences.end();
- bootCount = 0;
- blinkLED(5, 100);
- }
- configTzTime(MYTZ, "");
- // Load configuration from NVS.
- preferences.begin("water_cfg", true);
- wateringSchedule = preferences.getUInt("schedule", 0);
- motorRunSeconds = preferences.getInt("motorTime", 10);
- preferences.end();
- if (wateringSchedule != 0) { Serial.println("Found existing configuration in NVS."); }
- bootCount++; Serial.printf("\n--- Boot #%d ---\n", bootCount); blinkLED(2, 100);
- long long sleepDurationSeconds = FALLBACK_SLEEP_SECONDS;
- bool timeIsValid = false;
- // --- Main Logic (unchanged) ---
- if (wateringSchedule == 0) {
- Serial.println("No schedule found. Starting Configuration Portal.");
- startConfigurationPortal();
- if (portalSucceeded) { timeIsValid = true; } else { timeIsValid = false; }
- } else {
- if (connectToWiFi()) {
- Serial.println("WiFi Connected. Syncing time...");
- syncTimeAndSetupBot(); timeIsValid = true;
- } else {
- Serial.println("WiFi failed. Proceeding autonomously."); timeIsValid = true;
- }
- }
- // --- Watering & Sleep Calculation Logic (unchanged) ---
- if (timeIsValid) {
- struct tm timeinfo;
- if (!getLocalTime(&timeinfo)) {
- Serial.println("Critical Error: Failed to obtain time. Using fallback sleep.");
- sleepDurationSeconds = FALLBACK_SLEEP_SECONDS;
- } else {
- Serial.printf("Current time (source: %s): %s", (WiFi.status() == WL_CONNECTED ? "NTP" : "Internal RTC"), asctime(&timeinfo));
- Serial.printf("Schedule Mask: %u, Motor Time: %d sec\n", wateringSchedule, motorRunSeconds);
- // =====================================================================================
- // --- NEW: Watering Window Logic to Mitigate Drift and Save Power ---
- // =====================================================================================
- bool shouldWaterNow = false;
- // Check if THIS hour is a scheduled hour and we are in the first 5 minutes (e.g., woke up at 08:02)
- if ((wateringSchedule & (1 << timeinfo.tm_hour)) && (timeinfo.tm_min < 5)) {
- shouldWaterNow = true;
- }
- // Check if the NEXT hour is a scheduled hour and we are in the last 5 minutes of THIS hour (e.g., woke up at 07:58)
- else {
- int next_hour = (timeinfo.tm_hour + 1) % 24;
- if ((wateringSchedule & (1 << next_hour)) && (timeinfo.tm_min >= 55)) {
- shouldWaterNow = true;
- Serial.println("Woke up a few minutes early, watering now to save a sleep cycle.");
- }
- }
- if (shouldWaterNow) {
- Serial.println("Watering condition met.");
- if (WiFi.status() == WL_CONNECTED) {
- char msg[128];
- // --- MODIFIED LINE ---
- // The message is cleaned up and the version tag "(v%s)" is added.
- snprintf(msg, sizeof(msg), "Watering at %02d:%02d for %d sec. (Boot #%d, v%s)",
- timeinfo.tm_hour, timeinfo.tm_min, motorRunSeconds, bootCount, FW_VERSION);
- sendTelegramMessage(msg);
- }
- runMotor();
- } else {
- Serial.println("Woke up between events. Calculating next sleep.");
- }
- // =====================================================================================
- // --- End of New Logic ---
- // =====================================================================================
- time_t now = time(nullptr); struct tm nextEventTime = timeinfo;
- nextEventTime.tm_min = 0; nextEventTime.tm_sec = 0; int nextHour = -1;
- for (int i = 1; i <= 24; i++) {
- int checkHour = (timeinfo.tm_hour + i) % 24;
- if (wateringSchedule & (1 << checkHour)) { nextHour = checkHour; break; }
- }
- if (nextHour != -1) {
- if (nextHour <= timeinfo.tm_hour) { nextEventTime.tm_mday++; }
- nextEventTime.tm_hour = nextHour;
- sleepDurationSeconds = mktime(&nextEventTime) - now;
- } else { sleepDurationSeconds = FALLBACK_SLEEP_SECONDS; }
- }
- }
- if (sleepDurationSeconds <= 0) {
- Serial.printf("Invalid sleep duration calculated (%llds). Using fallback.\n", sleepDurationSeconds);
- sleepDurationSeconds = FALLBACK_SLEEP_SECONDS;
- }
- Serial.printf("Going to sleep for %lld seconds...\n", sleepDurationSeconds);
- // --- CONFIGURE WAKEUP SOURCES ---
- esp_sleep_enable_timer_wakeup(sleepDurationSeconds * uS_TO_S_FACTOR);
- // C3 CHANGE: Use the C3-specific GPIO wakeup function.
- // The first argument is a bitmask of the pins to wake up on.
- // 1ULL << resetButtonPin creates a bitmask with only the bit for GPIO 4 set.
- esp_deep_sleep_enable_gpio_wakeup(1ULL << resetButtonPin, ESP_GPIO_WAKEUP_GPIO_HIGH);
- blinkLED(3, 200); Serial.flush();
- esp_deep_sleep_start();
- }
- // --- Helper Functions ---
- void loop() {}
- void runMotor() {
- Serial.printf("Motor running for %d seconds...\n", motorRunSeconds);
- digitalWrite(enableMotorPin, HIGH);
- delay(motorRunSeconds * 1000);
- digitalWrite(enableMotorPin, LOW);
- Serial.println("Motor stopped.");
- }
- void blinkLED(int numBlinks, int blinkInterval) {
- for (int i = 0; i < numBlinks; i++) {
- digitalWrite(ledPin, LOW); delay(blinkInterval);
- digitalWrite(ledPin, HIGH); delay(blinkInterval);
- }
- }
- bool connectToWiFi() {
- Serial.print("Connecting to WiFi...");
- WiFi.mode(WIFI_STA);
- WiFi.begin(network, pass);
- int counter = 0;
- while (WiFi.status() != WL_CONNECTED) {
- delay(500); Serial.print(".");
- if (++counter >= 20) { // 10-second timeout
- Serial.println(" Timeout!"); return false;
- }
- }
- Serial.println(" Connected!"); return true;
- }
- void syncTimeAndSetupBot() {
- configTzTime(MYTZ, "pool.ntp.org", "time.google.com");
- struct tm timeinfo;
- while (!getLocalTime(&timeinfo, 5000)) {
- Serial.print(".");
- delay(500);
- }
- Serial.println(" Time Synced!");
- client.setCACert(telegram_cert);
- myBot.setTelegramToken(token);
- }
- void sendTelegramMessage(const char* msg) {
- if (WiFi.status() != WL_CONNECTED) return;
- Serial.printf("Sending to Telegram: \"%s\"\n", msg);
- if (myBot.begin()) {
- myBot.sendTo(userid, msg);
- }
- else {
- Serial.println("Failed to initialize Telegram Bot.");
- }
- }
- void startConfigurationPortal() {
- const char* ap_ssid = "WateringSystem_Setup";
- WiFi.softAP(ap_ssid);
- IPAddress apIP = WiFi.softAPIP();
- Serial.printf("AP started. Connect to %s\n", ap_ssid);
- dnsServer.start(53, "*", apIP);
- server.on("/", HTTP_GET, handleRoot);
- server.on("/save", HTTP_POST, handleSave);
- server.onNotFound(handleNotFound);
- server.begin();
- Serial.printf("Portal active for %lu minutes...\n", PORTAL_TIMEOUT_MS / 60000);
- unsigned long portalStartTime = millis();
- while (!portalSucceeded && (millis() - portalStartTime < PORTAL_TIMEOUT_MS)) {
- dnsServer.processNextRequest(); server.handleClient(); delay(10);
- }
- server.stop(); dnsServer.stop(); WiFi.softAPdisconnect(true); WiFi.mode(WIFI_OFF);
- Serial.println("Configuration portal closed.");
- }
- // =====================================================================================
- // WEB PAGE ASSETS
- // =====================================================================================
- // By defining the static CSS and JS here, we make the handleRoot() function much
- // cleaner and easier to manage. They are stored in PROGMEM to save RAM.
- const char PAGE_STYLE[] PROGMEM = R"rawliteral(
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background-color: #f0f2f5;
- color: #1c1e21;
- margin: 0;
- padding: 20px;
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- box-sizing: border-box;
- }
- .container {
- background-color: #fff;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.1);
- padding: 24px;
- width: 100%;
- max-width: 500px;
- box-sizing: border-box;
- }
- h1 {
- font-size: 24px;
- color: #1877f2;
- border-bottom: 1px solid #dddfe2;
- padding-bottom: 15px;
- margin: -24px -24px 20px -24px;
- padding-left: 24px;
- }
- .form-group { text-align: left; margin-bottom: 20px; }
- label {
- font-weight: bold;
- display: block;
- margin-bottom: 8px;
- font-size: 15px;
- }
- input[type="number"] {
- width: 100%;
- padding: 10px;
- border-radius: 6px;
- border: 1px solid #dddfe2;
- box-sizing: border-box;
- font-size: 16px;
- }
- .grid-container {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
- gap: 10px;
- }
- .grid-item {
- position: relative;
- background-color: #f0f2f5;
- border: 1px solid #dddfe2;
- border-radius: 6px;
- padding: 10px;
- cursor: pointer;
- user-select: none;
- transition: background-color 0.2s, border-color 0.2s;
- text-align: center;
- }
- .grid-item:hover { background-color: #e4e6eb; }
- .grid-item input { display: none; }
- .grid-item span { font-size: 14px; position: relative; z-index: 1; }
- .grid-item input:checked + span { font-weight: bold; color: #fff; }
- .grid-item input:checked ~ .checkmark { background-color: #1877f2; border-color: #1877f2; }
- .checkmark {
- display: block; height: 100%; width: 100%; position: absolute; top: 0;
- left: 0; z-index: 0; border-radius: 6px;
- }
- button {
- background-color: #1877f2;
- color: white;
- width: 100%;
- padding: 12px 0;
- font-size: 17px;
- font-weight: bold;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- transition: background-color 0.2s;
- }
- button:hover { background-color: #166fe5; }
- button:disabled { background-color: #a0b9d9; cursor: not-allowed; }
- #status {
- margin-top: 15px;
- font-weight: bold;
- color: #d9534f; /* Red for errors by default */
- }
- )rawliteral";
- const char PAGE_SCRIPT[] PROGMEM = R"rawliteral(
- document.addEventListener('DOMContentLoaded', function() {
- const form = document.getElementById('configForm');
- const motorTimeInput = document.getElementById('motorTime');
- const scheduleCheckboxes = document.querySelectorAll('input[name="schedule"]');
- const statusEl = document.getElementById('status');
- const submitBtn = document.querySelector('button[type="submit"]');
- form.addEventListener('submit', function(event) {
- // --- Client-Side Validation (Robustness & Security) ---
- statusEl.textContent = ''; // Clear previous errors
- // 1. Validate Motor Run Time
- const motorTime = parseInt(motorTimeInput.value, 10);
- if (isNaN(motorTime) || motorTime < 5 || motorTime > 300) {
- statusEl.textContent = 'Error: Motor run time must be between 5 and 300.';
- event.preventDefault(); // Stop form submission
- return;
- }
- // 2. Validate Schedule Selection
- let isAnyChecked = false;
- scheduleCheckboxes.forEach(function(checkbox) {
- if (checkbox.checked) {
- isAnyChecked = true;
- }
- });
- if (!isAnyChecked) {
- statusEl.textContent = 'Error: Please select at least one watering hour.';
- event.preventDefault(); // Stop form submission
- return;
- }
- // If validation passes, add the timestamp
- document.getElementById('timestamp').value = Math.floor(new Date().getTime() / 1000);
- // Give user feedback
- submitBtn.textContent = 'Saving...';
- submitBtn.disabled = true;
- });
- });
- )rawliteral";
- void handleRoot() {
- // This function is now much more readable. It acts as a simple templating engine.
- String html = "<!DOCTYPE html><html><head>";
- html += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";
- html += "<title>Watering System Setup</title>";
- // Inject the CSS
- html += "<style>";
- html += FPSTR(PAGE_STYLE);
- html += "</style>";
- html += "</head><body><div class='container'>";
- html += "<h1>Watering System Setup</h1>";
- html += "<form id='configForm' action='/save' method='post'>";
- // --- Dynamic Part 1: Motor Run Time ---
- html += "<div class='form-group'><label for='motorTime'>Motor Run Time (5-300 seconds):</label>";
- html += "<input type='number' id='motorTime' name='motorTime' min='5' max='300' value='";
- html += motorRunSeconds;
- html += "' required></div>";
- // --- Dynamic Part 2: Watering Schedule Grid ---
- html += "<div class='form-group'><label>Watering Schedule (Hours):</label>";
- html += "<div class='grid-container'>";
- for (int i = 0; i < 24; i++) {
- html += "<label class='grid-item'><input type='checkbox' name='schedule' value='" + String(i) + "'";
- if (wateringSchedule & (1 << i)) { html += " checked"; }
- char hourStr[4];
- snprintf(hourStr, sizeof(hourStr), "%02d", i);
- html += "><span>" + String(hourStr) + ":00</span><div class='checkmark'></div></label>";
- }
- html += "</div></div>"; // Close grid-container and form-group
- // --- Form Footer ---
- html += "<input type='hidden' id='timestamp' name='timestamp' value=''>";
- html += "<button type='submit'>Save Settings & Sync Time</button>";
- html += "<p id='status'></p>"; // For validation messages
- html += "</form></div>";
- // Inject the JavaScript
- html += "<script>";
- html += FPSTR(PAGE_SCRIPT);
- html += "</script>";
- html += "</body></html>";
- server.send(200, "text/html", html);
- }
- void handleSave() {
- preferences.begin("water_cfg", false); // false = read/write mode
- // Save Motor Run Time
- if (server.hasArg("motorTime")) {
- int newMotorTime = server.arg("motorTime").toInt();
- if (newMotorTime >= 5 && newMotorTime <= 300) {
- motorRunSeconds = newMotorTime;
- preferences.putInt("motorTime", motorRunSeconds);
- Serial.printf("Saved new motor time to NVS: %d sec\n", motorRunSeconds);
- }
- }
- // Save Schedule Bitmask
- uint32_t newSchedule = 0;
- for (int i = 0; i < server.args(); i++) {
- if (server.argName(i) == "schedule") {
- int hour = server.arg(i).toInt();
- if (hour >= 0 && hour <= 23) {
- newSchedule |= (1 << hour);
- }
- }
- }
- wateringSchedule = newSchedule;
- preferences.putUInt("schedule", wateringSchedule);
- Serial.printf("Saved new schedule mask to NVS: %u\n", wateringSchedule);
- // IMPORTANT: Close preferences to commit the changes to flash memory!
- preferences.end();
- // Set the system time from the phone's timestamp
- if (server.hasArg("timestamp")) {
- long long timestamp = atoll(server.arg("timestamp").c_str());
- struct timeval tv; tv.tv_sec = timestamp; tv.tv_usec = 0;
- settimeofday(&tv, NULL);
- Serial.printf("Time set from phone: %lld\n", timestamp);
- }
- portalSucceeded = true;
- String successPage = R"rawliteral(<!DOCTYPE html><html><head><title>Success</title><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>body{font-family:sans-serif;text-align:center;padding-top:50px}</style></head><body><h1>Success!</h1><p>Settings saved. The device will now go to sleep.</p></body></html>)rawliteral";
- server.send(200, "text/html", successPage);
- delay(1000);
- }
- void handleNotFound() {
- server.sendHeader("Location", "/", true);
- server.send(302, "text/plain", "");
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement