This code manages an ESP32 device for plant care. The system includes 6 watering valves, 6 rain sensors, 1 water pump, 1 water level sensor, 1 master overflow sensor, and 1 relay-controlled plant lamp with automatic night schedule support.
Induction copper plates water level
Version 1.19.10 - fixes Telegram menu registration, startup notification retry, and lamp boot sync
Recent Updates:
- v1.19.10: Fixed Telegram bot menu registration when using the monitoring proxy by adding
setMyCommandssupport to the proxy, retried startupDevice Onlinenotification through the queue when the first send fails, and initialized the plant light to the current auto-schedule state without emitting a duplicate boot-timePLANT LIGHT ONtransition. - v1.19.6: Fixed critical boot catch-up safety bug where reboot recovery watered all trays when any tray was overdue. Boot catch-up now targets only overdue trays and Telegram shows
Boot Catch-up/Boot Calibrationinstead of misleadingMQTT. - v1.19.5: Fixed Telegram notification queue delivery so completion and updated schedule messages are retried instead of being dropped on transient Telegram/proxy send failures or cooldown windows.
- v1.19.4: Watering schedule output now shows an expected runtime during retry or calibration-in-progress states by falling back to the per-valve normal timeout when baseline fill time is not available yet.
- v1.19.3: Increased Tray 2 timeout from 30s to 35s after observed timeout at ~30.7s. Emergency timeout for Tray 2 was increased from 35s to 40s to keep the +5s safety margin.
- v1.19.2: Reset watering cycle by rotating learning-data filenames. Active file is now
learning_data_v1.19.2.json, and previouslearning_data_v1.18.4.jsonis marked as OLD for one-time cleanup on boot. - v1.19.1: Fixed Telegram debug feedback loop where internal transport logs (e.g.
β Telegram message sent) were routed back into Telegram debug queue, causing periodic self-generated spam every few seconds. Transport-layer logs are now serial-only. - v1.19.0: Added Telegram command polling throttle (
TELEGRAM_COMMAND_POLL_INTERVAL_MS=1000) sogetUpdates(timeout=0)is not called every 100ms network loop tick. Reduces continuousssl_client (-76)andWiFiClient ERR:9noise caused by rapid TLS reconnect churn while keeping command checks responsive. - v1.18.9: Switched recommended monitoring deployment to nginx TLS termination on
:16443with Python proxy on localhost127.0.0.1:18085. This avoids direct Python TLS serving and reduces ESP32 TLS-close noise (ssl_client -76) while keeping auth/token logic in proxy. - v1.18.8: Added separate proxy-mode HTTP timeout (
TELEGRAM_PROXY_HTTP_TIMEOUT_MS=4000) while keeping direct Telegram timeout at1500ms. Fixes repeated ESP32 SSL read timeouts when proxy->Telegram responses take longer than 1.5 seconds. - v1.18.7: Fixed monitoring proxy deployment docs for non-root service TLS key access by using service-readable cert/key copies under
/etc/telegram-bot-api-proxy. Verified live endpoint onhttps://water-the-flowers-proxy.example.com:16443/health. - v1.18.6: Updated monitoring proxy service setup docs/env examples to use
water-the-flowers-proxy.example.comcertificate paths and added explicit reboot-protection verification steps (systemctl is-enabled+ post-reboot health check). - v1.18.5: Added Telegram Bot API proxy mode for ESP32 (
sendMessage+getUpdates) with optional bearer auth, custom HTTPS port configuration, and monitoring-host systemd deployment files. Updated defaults/docs to avoid VPN relay conflict by using16443(not15443). - v1.18.4: just version up
- v1.18.3: Made network task local-first so OTA/web API remain responsive during internet outages or Telegram restrictions. Added Telegram fast timeout + exponential cooldown to avoid repeated blocking. Limited queued Telegram notification processing to one per cycle so watering/lamp logic stays responsive even when Telegram/MQTT fail.
- v1.18.2: Updated project documentation to reflect the active-low plant lamp relay, Telegram/API lamp controls, dashboard behavior, filesystem upload workflow, and current data file/version details.
- v1.18.1: Fixed plant light relay polarity (active-low), so Telegram/manual commands and the 22:00 -> 07:00 schedule now match physical ON/OFF behavior. Added dashboard controls via
/api/lamp. - v1.18.0: Added plant light relay control on a free ESP32 GPIO with automatic overnight schedule (22:00 -> 07:00), Telegram commands (
/lamp,/lamp_on,/lamp_off,/lamp_auto), and MQTT/web status reporting. - v1.16.2: Added GPIO hardware reinitialization to fix stuck relay modules after emergency events. New
/reinit_gpiocommand (Telegram/MQTT). Automatic GPIO reset after overflow/water level recovery. Learning algorithm threshold adjusted from 95% to 85% for better calibration. - v1.15.6: Added 10-second confirmation delay to water level sensor - prevents false low-water alarms when water drains back from pipes to tank after pump stops
- v1.15.5: Reset learning data files to force immediate recalibration after bug fixes
- v1.15.4: Fixed overflow recovery learning + long outage boot detection - prevents interval doubling after overflow events and ensures immediate watering after extended power outages
- v1.14.0: Added water level sensor (GPIO 19, moved to GPIO 21 in v1.26.5) - automatically blocks watering when tank is empty, auto-resumes when refilled, sends Telegram notifications
- v1.13.4: Fixed rain sensor reading bug - production code now correctly powers both valve pin + GPIO 18 (not just GPIO 18)
The state machine logic has been extracted into a separate, hardware-independent module for better testability and maintainability:
StateMachineLogic.h - Pure state machine logic:
- Phase transitions: Idle β Opening β Stabilization β Rain Check β Watering β Closing
- Built-in safety: Timeout handling (normal 25s, emergency 30s)
- Hardware-independent: No direct GPIO operations, returns actions to execute
- Pure functions: Easily testable without ESP32 hardware
Benefits:
- β Unit testable on desktop (no ESP32 required)
- β Clear separation of logic and hardware
- β Easier to reason about state transitions
- β Better reliability through comprehensive testing
The learning algorithm has been separated into reusable helper functions:
LearningAlgorithm.h - Time-based learning helpers:
calculateWaterLevelBefore()- Calculate water level from fill duration ratiocalculateEmptyDuration()- Estimate time until tray is empty based on consumptionformatDuration()- Human-readable duration formatting (e.g., "2d 4h")
Benefits:
- β Reusable across different components
- β Independently testable
- β No hardware dependencies
- β Clear, documented algorithms
New native testing framework allows testing logic without hardware:
Test Files:
test/test_native_all.cpp- Combined test suite (20 tests)test/test_state_machine.cpp- State machine specific tests (17 tests)test/test_learning_algorithm.cpp- Learning algorithm tests (3 tests)test/test_overwatering_scenarios.cpp- Safety scenario tests
Test Coverage:
- β All state machine phase transitions
- β Timeout handling (normal & emergency)
- β Full watering cycles from start to finish
- β Learning algorithm calculations
- β Overwatering scenarios and safety measures
Run Tests:
# Run all native tests on your computer (no ESP32 required)
pio test -e nativeDocumentation:
NATIVE_TESTING_PLAN.md- Testing strategy and frameworkOVERWATERING_RISK_ANALYSIS.md- Safety analysis and mitigationOVERWATERING_TEST_SUMMARY.md- Test results and validation
The system now includes 7 independent safety layers to prevent overwatering and ensure safe operation:
Layer 1: Master Overflow Sensor (v1.12.1)
- Hardware: Rain sensor on GPIO 42 (2N2222 transistor circuit)
- Detection: LOW = overflow detected, HIGH = normal
- Response Time: 100ms polling (fastest safety check)
- Emergency Actions:
- Immediate shutdown of all valves via direct GPIO control
- Pump stopped immediately
- All watering operations blocked
- Telegram alert sent with emergency details
- Recovery: Manual intervention required, send
/reset_overflowcommand
Layer 2: Water Level Sensor (v1.14.0, v1.15.6 delay)
- Hardware: Float switch on GPIO 21 (monitors water tank level)
- Detection: HIGH = water OK, LOW = tank empty
- Response Time: 100ms polling
- Confirmation Delay (v1.15.6): 10-second delay before blocking watering - prevents false alarms from pipe drainage after pump stops
- Automatic Actions:
- Blocks all watering operations when tank is empty (after 10s confirmation)
- Stops active watering immediately if water runs out
- Sends Telegram notification on low water (after delay expires)
- Sends Telegram notification when water is restored
- Automatically cancels blocking if water level rises during 10s delay (pipe drainage detected)
- Recovery: Automatic - system resumes normal operation when tank is refilled (no manual intervention needed)
Layer 3: Safety Timeouts (v1.12.5)
- MAX_WATERING_TIME: 25 seconds (normal watering timeout)
- ABSOLUTE_SAFETY_TIMEOUT: 30 seconds (emergency hard limit)
Layer 4: Two-Tier State Machine Timeouts
- Normal timeout (25s): Standard valve closure with learning data processing
- Emergency cutoff (30s): Forces hardware shutdown via direct GPIO control
Layer 5: Global Safety Watchdog
- Runs independently every loop iteration
- Bypasses state machine if timeout exceeded
- Forces valves/pump OFF directly via GPIO
- Cannot be blocked by state machine issues
Layer 6: Enhanced Sensor Logging
- Logs raw GPIO values every 5 seconds during watering
- Tracks sensor readings for post-incident analysis
- Helps diagnose hardware failures
Layer 7: Sensor Diagnostic Tools
test_sensors- Test all 6 sensors and generate reporttest_sensor_N(N=0-5) - Test individual sensor- Detects pullup resistor failures
- Shows power-on/off readings to identify shorts
10-Second Boot Countdown for Emergency Updates:
Every boot provides a safety window for firmware updates:
- Device boots and connects to WiFi
- Sends Telegram notification: "Starting in 10 seconds... Send /halt to prevent operations"
- Polls for
/haltcommand every 500ms - If
/haltreceived β enters halt mode (blocks all watering) - If countdown expires β normal operation
Halt Mode Features:
- β Blocks ALL watering operations (manual, sequential, auto-watering)
- β Stops any active watering immediately
- β Provides OTA firmware update URL
- β Can be activated anytime (not just during boot)
- β
Exit with
/resumecommand
Use Cases:
- Emergency firmware fix after discovering critical bug
- Quick access to OTA without waiting for watering cycle
- Block operations while testing/debugging remotely
Commands:
/halt- Enter halt mode (via Telegram or MQTT)/resume- Exit halt mode (via Telegram or MQTT)
Hardware Circuit:
- Rain sensor detects water overflow from trays
- Connected via 2N2222 transistor circuit to GPIO 42
- Pulls GPIO LOW when water detected (overflow condition)
Safety Features:
- β Highest priority check - Runs first in every loop (100ms polling)
- β Immediate emergency stop - Direct GPIO control bypasses all state machines
- β Comprehensive shutdown - Closes all valves, stops pump, blocks all future watering
- β Telegram emergency alert - Sends detailed notification with actions taken
- β Manual recovery required - Prevents automatic restart after overflow
Emergency Response: When overflow detected:
- All valves closed immediately via direct GPIO writes
- Pump stopped
- LED turned off
- Sequential mode terminated
- All future watering attempts blocked
- Telegram notification sent with timestamp and recovery instructions
Recovery Process:
- Physically fix the overflow issue (empty trays, check for blockage)
- Send MQTT command:
reset_overflowor/reset_overflow - Or use Telegram:
/reset_overflow - System resumes normal operation
Example Telegram Alert:
π¨π¨π¨ WATER OVERFLOW DETECTED π¨π¨π¨
β° 10-01-2026 14:23:45
π§ Master overflow sensor triggered
π§ Water is overflowing from tray!
β
Emergency actions taken:
β’ All valves CLOSED
β’ Pump STOPPED
β’ System LOCKED
β οΈ Manual intervention required!
Send /reset_overflow to resume operations
Hardware Circuit:
- Float switch in water tank connected to GPIO 21
- Pulls GPIO HIGH when water present (tank OK)
- Pulls GPIO LOW when no water (tank empty)
Safety Features:
- β High priority check - Runs second in every loop (100ms polling, right after overflow sensor)
- β Automatic blocking - Prevents watering when tank is empty
- β Emergency stop - Stops active watering if water runs out mid-cycle
- β Telegram notifications - Alerts on low water and when restored
- β Automatic recovery - Resumes normal operation when tank is refilled (no manual intervention)
Automatic Response: When water level low detected:
- All future watering attempts blocked (manual, sequential, auto-watering)
- If watering is active: valves closed immediately, pump stopped
- Telegram notification sent with low water alert
- System continuously monitors for water restoration
- When water restored: Telegram notification sent, normal operation resumes automatically
Example Telegram Alerts:
Low Water Alert:
β οΈβ οΈβ οΈ WATER LEVEL LOW β οΈβ οΈβ οΈ
β° 12-01-2026 10:15:32
π§ Water tank is empty or low
π§ Sensor GPIO 21
β±οΈ Confirmed after 10s delay
β
Actions taken:
β’ All valves CLOSED
β’ Pump STOPPED
β’ Watering BLOCKED
π System will resume automatically when water is refilled
Water Restored:
β
WATER LEVEL RESTORED β
β° 12-01-2026 10:45:18
π§ Water tank refilled
π System resuming normal operation
β Watering operations enabled
The system uses a 5-phase watering cycle per valve:
- Open Valve First - Rain sensors require water flow to function accurately
- Wait for Stabilization - 500ms delay for water to start flowing
- Check Rain Sensor - Now accurate with flowing water
- If already wet: Close valve, abort (pump never starts)
- If dry: Proceed to watering
- Watering Phase - Turn on pump, monitor sensor every 100ms
- Stop when sensor detects water OR 15s timeout
- Close Valve - Turn off pump if no other valves active
This algorithm is used separately for each of 6 valves. State publishes to MQTT topic on each state change. Errors in producing messages to MQTT topics don't affect the algorithm itself.
The system automatically learns when each tray is empty and waters accordingly:
Time-Based Approach:
- Tracks actual time duration instead of counting cycles
- Learns three key metrics per tray:
- Baseline fill time - Time to fill from completely empty
- Empty-to-full duration - How long tray takes to consume all water
- Current water level - Estimated based on time elapsed
Adaptive Baseline:
- First watering establishes initial baseline
- Baseline auto-updates when a longer fill is observed (tray was emptier)
- Uses weighted averaging (70% old, 30% new) for stability
Automatic Watering:
- System checks each tray continuously
- When
time_since_last_watering >= empty_to_full_durationβ Auto-water - Works independently for each valve
- Can be enabled/disabled per valve
Example:
Watering 1: Fill 5.0s β Baseline: 5.0s (initial calibration)
Watering 2: Fill 4.2s β Baseline: 5.0s, Water before: 16%, Empty time: 24h
Watering 3: Fill 3.8s β Baseline: 5.0s, Water before: 24%, Empty time: ~22h
Watering 4: Fill 5.2s β Baseline: 5.2s β¨ (tray was emptier, baseline updated)
Watering 5: Fill 3.8s β Baseline: 5.2s, Water before: 27%, System stable
Benefits:
- β Automatic watering when trays are empty
- β Adapts to different tray sizes (different baselines)
- β Learns consumption rate (varying temperatures, plant needs)
- β Each valve operates independently
- β Data persists across reboots (saved to flash)
- β Shows estimated water level percentage and time until empty
- Learning data automatically saved to LittleFS (
/learning_data.json) - Survives ESP32 reboots
- Handles millis() overflow (49-day wraparound)
- Manual trigger: Changes saved after each successful watering
Learning data is published in MQTT state updates under each valve's learning object.
Overflow Recovery Protection: The system now intelligently handles scenarios where overflow blocks scheduled watering:
Problem Scenario:
- Overflow detected (system locks)
- Scheduled watering time passes (watering blocked by overflow)
- User sends
/reset_overflowcommand - Tray found wet (possibly from rain or manual watering during overflow period)
- OLD BEHAVIOR: System incorrectly doubles watering interval (thinking plant consumed water slowly)
- NEW BEHAVIOR: System detects recent overflow reset, skips learning without penalty
Implementation:
- Tracks
lastOverflowResetTimewhen overflow is reset - 2-hour grace period (
OVERFLOW_RECOVERY_THRESHOLD_MS) after overflow reset - If tray found wet within grace period: Skip cycle, no interval change
- Prevents incorrect learning from overflow-blocked watering cycles
Debug Output:
π§ OVERFLOW RECOVERY DETECTION: Tray wet after overflow reset
Time since overflow reset: 23s
Skipping cycle (no interval change) - watering was blocked by overflow
Long Outage Boot Detection: The system now correctly handles watering after extended power outages:
Problem Scenario:
- System last watered on Day 0
- Power outage for 3+ days (longer than millis() can represent)
- System reboots on Day 3
- OLD BEHAVIOR:
lastWateringCompleteTimeset to 0 (can't represent in millis),hasOverdueValves()returns false, no watering triggered - NEW BEHAVIOR: Stores
realTimeSinceLastWateringduration, boot logic detects overdue, immediate catch-up watering
Implementation:
- New field:
ValveController.realTimeSinceLastWatering(stores duration when timestamp can't fit in millis) loadLearningData(): WhencurrentMillis < timeSinceWatering, stores real duration instead of 0hasOverdueValves(): Checks bothlastWateringCompleteTimeandrealTimeSinceLastWateringshouldWaterNow(): Uses real duration if timestamp is 0
Debug Output:
Valve 2 is overdue (interval: 2d 0h)
Overdue valves detected - starting catch-up watering
Benefits:
- β No false interval doubling after overflow events
- β Reliable watering after extended power outages (days/weeks)
- β Correct learning behavior in all edge cases
- β Maintains schedule stability through disruptions
The system sends automatic notifications to your Telegram bot during sequential watering sessions.
Sent when watering begins:
πΏ Watering Started
β° Session 16-11-2025 19:28:35
π§ Trigger: MQTT
π± Trays: All
Sent when all valves finish:
β
Watering Complete
tray | duration(sec) | status
-----|---------------|-------
6 | 3.2 | β OK
5 | 4.5 | β OK
4 | 0.5 | β οΈ ALREADY_WET
3 | 14.5 | β οΈ TIMEOUT
2 | 2.8 | β οΈ MANUAL_STOP
1 | 3.8 | β OK
Status Types (v1.6.1 updated):
β OK- Watering completed successfully (sensor became wet after pump started)β FULL- Tray was already full (sensor already wet before pump started)β οΈ TIMEOUT- Exceeded 25s maximum watering timeβ οΈ STOPPED- Watering stopped manually or other interruption
Configuration (in include/secret.h):
#define TELEGRAM_BOT_TOKEN "your_bot_token"
#define TELEGRAM_CHAT_ID "your_chat_id"
// Optional: route Telegram through monitoring server proxy
// Leave empty ("") to use direct https://api.telegram.org.
#define TELEGRAM_PROXY_BASE_URL "https://monitoring.example.com:16443"
#define TELEGRAM_PROXY_AUTH_TOKEN "your_proxy_api_token"Features:
- Real date/time via NTP sync (GMT+3 Moscow timezone)
- Only triggers during sequential watering (not individual valves)
- Works over WiFi using Telegram Bot API
- Properly aligned table in monospace format
The system includes a sophisticated debug message delivery system with automatic retry and message grouping.
- Circular buffer queue - Holds up to 20 messages
- Automatic retry - Up to 5 attempts per message with 2-second delays
- Non-blocking - Processes one message per loop iteration
- Failure handling - Messages dropped after 5 failed attempts
Debug messages arriving close together are automatically grouped:
- 2-second grouping window - Messages within 2s are batched
- 3-minute safety limit - Groups flush after 3 minutes max (prevents infinite buffering)
- Explicit flush on completion - All buffered messages sent before watering complete notification
- Timestamped - Each message shows exact time:
[DD-MM-YYYY HH:MM:SS.mmm]
#define IS_DEBUG_TO_SERIAL_ENABLED false // Enable serial console debug
#define IS_DEBUG_TO_TELEGRAM_ENABLED true // Enable Telegram debug
const int TELEGRAM_QUEUE_SIZE = 20;
const int TELEGRAM_MAX_RETRY_ATTEMPTS = 5;
const unsigned long TELEGRAM_RETRY_DELAY_MS = 2000;
const unsigned long MESSAGE_GROUP_INTERVAL_MS = 2000;
const unsigned long MESSAGE_GROUP_MAX_AGE_MS = 180000; // 3 minutesπ Debug
[17-11-2025 14:23:10.125] β Valve 0 opened - waiting stabilization
[17-11-2025 14:23:10.625] Step 2: Checking rain sensor (water is flowing now)...
[17-11-2025 14:23:10.725] β Sensor 0 is DRY - starting pump (timeout: 20s)
[17-11-2025 14:23:11.825] Valve 0: 1s/19s, Sensor: DRY
[17-11-2025 14:23:12.925] Valve 0: 2s/18s, Sensor: DRY
[17-11-2025 14:23:13.125] β Valve 0 COMPLETE - Total: 3s (pump: 2s)
Code was generated in Claude
This project uses two separate firmware builds for clean separation between production and testing.
| Environment | Source File | Purpose | Flash Usage |
|---|---|---|---|
esp32-s3-devkitc-1 |
src/main.cpp |
Production watering system | ~80% (1055 KB) |
esp32-s3-devkitc-1-test |
src/test-main.cpp |
Hardware testing with OTA | ~63% (824 KB) |
Build and upload:
platformio run -t upload -e esp32-s3-devkitc-1
platformio device monitor -b 115200 --rawFeatures:
- Full watering system with time-based learning
- WiFi, MQTT, Telegram notifications
- Web interface for control and OTA updates
- Persistent learning data in LittleFS
Excludes: Test code (saves ~750 KB flash, better stability)
Build and upload:
# Upload firmware (filesystem already contains test HTML)
platformio run -t upload -e esp32-s3-devkitc-1-test
# Monitor serial output
platformio device monitor -b 115200 --rawNote: Both production and test firmware share the same filesystem. Upload filesystem once with production firmware, then switch modes without re-uploading filesystem to preserve learning data.
Features:
- π Web Dashboard at
http://<device-ip>/dashboard- Real-time serial output via WebSocket
- Interactive test buttons for all commands
- Individual sensor testing (R1-R6 for single read, M1-M6 for continuous monitor)
- Auto-scrolling console with color-coded output
- Connection status indicator
- No serial cable needed for testing!
- Interactive serial menu (press
Hfor help) - Test all hardware: LED, pump, 6 valves, 6 rain sensors (individually or all at once)
- Test DS3231 RTC (I2C at GPIO 14/SDA, GPIO 3/SCL) with current time sync
- Test water level sensor (GPIO 21)
- Test master overflow sensor (GPIO 42)
- I2C bus scanner
- WiFi & OTA support - remotely switch back to production firmware
Hardware Components:
L- Toggle LEDP- Toggle pump1-6- Toggle individual valvesA/Z- All valves on/offX- Emergency stop (turn everything off)
Rain Sensors (All):
R- Read all sensors onceM- Monitor all sensors continuouslyS- Stop monitoring
Rain Sensors (Individual):
R1-R6- Read specific sensor once (e.g.,R1= Sensor 1: Valve GPIO 5, Sensor GPIO 8)M1-M6- Monitor specific sensor continuously (e.g.,M6= Sensor 6: Valve GPIO 17, Sensor GPIO 13)S- Stop monitoring and power off
Individual Sensor GPIO Mapping:
- Sensor 1: Valve GPIO 5, Sensor GPIO 8
- Sensor 2: Valve GPIO 6, Sensor GPIO 9
- Sensor 3: Valve GPIO 7, Sensor GPIO 10
- Sensor 4: Valve GPIO 15, Sensor GPIO 11
- Sensor 5: Valve GPIO 16, Sensor GPIO 12
- Sensor 6: Valve GPIO 17, Sensor GPIO 13
Water Level Sensor (GPIO 21):
W- Read water level sensor once (HIGH = water OK, LOW = empty)N- Monitor water level sensor continuouslyS- Stop monitoring
Master Overflow Sensor (GPIO 42):
O- Read overflow sensor onceV- Monitor overflow sensor continuouslyS- Stop monitoring
DS3231 RTC (I2C):
T- Read time/date/temperatureI- Scan I2C bus for devicesU- Set RTC to current time (use web dashboard)K- Reset RTC to epoch (2000-01-01 00:00:00)B- Read battery voltage (VBAT)
System:
F- Full sequence test (all components including water level sensor)H- Show menu
# Switch to test mode (firmware only - preserves learning data!)
platformio run -t upload -e esp32-s3-devkitc-1-test
# Switch back to production (firmware only - preserves learning data!)
platformio run -t upload -e esp32-s3-devkitc-1Important: Only upload firmware, not filesystem! Both modes share the same filesystem, which contains your learning data.
Both firmware modes support OTA! You can switch remotely:
- Build firmware:
platformio run -e <environment> - Access OTA page:
- Production mode:
http://<device-ip>/firmware(login required) - Test mode:
http://<device-ip>/firmware(login required)
- Production mode:
- Upload firmware file:
- To switch to production:
.pio/build/esp32-s3-devkitc-1/firmware.bin - To switch to test:
.pio/build/esp32-s3-devkitc-1-test/firmware.bin
- To switch to production:
- Device automatically reboots into new firmware
β Industry Best Practice - Standard for embedded systems β No Code Bloat - Production excludes test code (saves 750 KB) β Zero Risk - Test code never runs in production β Better Stability - Smaller binary = more reliable β Clear Separation - Easier maintenance and debugging
Both firmware modes share the same LittleFS filesystem:
/web/
βββ prod/ # Production UI
β βββ index.html
β βββ css/style.css
β βββ js/app.js
βββ test/ # Test mode UI
βββ index.html
βββ firmware.html
/learning_data_v1.8.7.json # Learning data (preserved!)
Key Benefits:
- β Upload filesystem ONCE (with production firmware)
- β Switch modes anytime without losing learning data
- β Learning data persists across firmware switches
β οΈ Only re-upload filesystem when updating web UI files
When to Upload Filesystem:
- Initial deployment
- Web UI updates (HTML/CSS/JS changes)
- After factory reset / flash erase
When NOT to Upload Filesystem:
- Switching between production/test modes
- Firmware updates only
- Any time you want to preserve learning data
# Verify all files exist
find . -type f -name "*.html" -o -name "*.css" -o -name "*.js" -o -name "*.h" -o -name "*.cpp"
# Expected structure:
data/web/index.html β MUST EXIST
data/web/css/style.css β MUST EXIST
data/web/js/app.js β MUST EXIST
include/secret.h β MUST EXIST (never commit)
include/ota.h β MUST EXIST
src/main.cpp β MUST EXIST
platformio.ini β MUST EXISTEdit include/secret.h:
#define SSID "your_wifi_name"
#define SSID_PASSWORD "your_wifi_password"
#define YC_DEVICE_ID "your_device_id"
#define MQTT_PASSWORD "your_mqtt_password"
#define OTA_USER "admin"
#define OTA_PASSWORD "your_ota_password"
#define TELEGRAM_BOT_TOKEN "your_bot_token"
#define TELEGRAM_CHAT_ID "your_chat_id"
// Optional monitoring-server proxy for Telegram (send + getUpdates)
// Keep TELEGRAM_PROXY_BASE_URL empty for direct Telegram access.
#define TELEGRAM_PROXY_BASE_URL "https://monitoring.example.com:16443"
#define TELEGRAM_PROXY_AUTH_TOKEN "your_proxy_api_token"When TELEGRAM_PROXY_BASE_URL is not empty, firmware routes both:
- outgoing messages (
sendMessage) - incoming commands (
getUpdates)
through monitoring server endpoints:
POST /v1/telegram/sendMessageGET /v1/telegram/getUpdates
Expected behavior:
- Proxy returns raw Telegram Bot API JSON body.
- HTTP
200means success. - Optional auth header:
Authorization: Bearer <TELEGRAM_PROXY_AUTH_TOKEN>.
Quick start on monitoring server:
python3 tools/telegram_bot_api_proxy.pyOptional env:
export TELEGRAM_PROXY_HOST=127.0.0.1
export TELEGRAM_PROXY_PORT=18085
export TELEGRAM_PROXY_AUTH_TOKEN=change_me
export TELEGRAM_PROXY_TLS_CERT_FILE=
export TELEGRAM_PROXY_TLS_KEY_FILE=
python3 tools/telegram_bot_api_proxy.pyHealth check:
curl -sk https://water-the-flowers-proxy.example.com:16443/health- Install files on monitoring server:
sudo mkdir -p /opt/iot-yc-water-the-flowers
sudo rsync -av ./ /opt/iot-yc-water-the-flowers/
sudo cp /opt/iot-yc-water-the-flowers/deploy/systemd/telegram-bot-api-proxy.service /etc/systemd/system/
sudo cp /opt/iot-yc-water-the-flowers/deploy/systemd/telegram-bot-api-proxy.env.example /etc/default/telegram-bot-api-proxy- Edit
/etc/default/telegram-bot-api-proxy:
- set
TELEGRAM_PROXY_HOST=127.0.0.1 - set
TELEGRAM_PROXY_PORT=18085 - set
TELEGRAM_PROXY_AUTH_TOKEN(long random token) - set
TELEGRAM_PROXY_TLS_CERT_FILE=(empty) - set
TELEGRAM_PROXY_TLS_KEY_FILE=(empty)
- Start and enable:
sudo systemctl daemon-reload
sudo systemctl enable --now telegram-bot-api-proxy.service
sudo systemctl status telegram-bot-api-proxy.service --no-pager
sudo systemctl is-enabled telegram-bot-api-proxy.service- Add nginx TLS termination on public
16443:
server {
listen 16443 ssl http2;
server_name water-the-flowers-proxy.example.com;
ssl_certificate /etc/letsencrypt/live/water-the-flowers-proxy.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/water-the-flowers-proxy.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://127.0.0.1:18085;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Apply nginx config:
sudo nginx -t
sudo systemctl reload nginx- Open firewall for custom SSL port:
sudo ufw allow 16443/tcp- Reboot protection verification:
sudo reboot
# after host is back:
systemctl status telegram-bot-api-proxy.service --no-pager
curl -sk https://water-the-flowers-proxy.example.com:16443/healthplatformio run -t clean -e esp32-s3-devkitc-1# This creates the LittleFS image from /data folder
platformio run -t buildfs -e esp32-s3-devkitc-1Output should show:
LittleFS Image Generator
Data directory: data/
Resolving files...
web/index.html (8.2 KB)
web/css/style.css (6.1 KB)
web/js/app.js (8.3 KB)
Size: 22.6 KB / 1536 KB (1.47%)
platformio run -t erase -e esp32-s3-devkitc-1platformio run -t uploadfs -e esp32-s3-devkitc-1Expected output:
esptool.py v3.x.x
Uploading .pio/build/esp32-s3-devkitc-1/littlefs.bin to address 0x3c0000 ...
Writing at 0x3c0000... (X%)
Wrote 24576 bytes at 0x3c0000 in Y seconds...
platformio run -t upload -e esp32-s3-devkitc-1platformio device monitor -b 115200 --rawExpected startup sequence:
=================================
Smart Watering System
Platform: ESP32-S3-DevKitC-1
Version: watering_system_1.4.0
Device ID: [your_device_id]
Valves: 6
=================================
WateringSystem initialized with 6 valves
...
LittleFS mounted successfully
Files in LittleFS:
- /web (directory)
- /web/index.html (8192 bytes)
- /web/css/style.css (6144 bytes)
- /web/js/app.js (8320 bytes)
Connecting to WiFi...
WiFi Connected!
IP Address: 192.168.x.x
Connecting to Yandex IoT Core...
MQTT Connected!
Setting up OTA...
Control Panel: http://esp32-watering.local/
Symptoms:
ÿÿÿÿýþ½ÿÿþ¾ûþýÿÿþ
Solutions:
- Check baud rate: Must be 115200
platformio device monitor -b 115200- Try with
--rawflag:
platformio device monitor -b 115200 --raw- Reset device manually and watch output
- In VS Code PlatformIO: Check Monitor Speed = 115200
Symptoms:
Attempting to serve: /web/index.html
ERROR: File not found: /web/index.html
Solutions:
- Verify file exists locally:
ls -la data/web/
# Should show: index.html, css/, js/
ls -la data/web/css/
# Should show: style.css
ls -la data/web/js/
# Should show: app.js- Rebuild filesystem image:
platformio run -t buildfs -e esp32-s3-devkitc-1 --verbose- Re-upload filesystem (erase first):
platformio run -t erase -e esp32-s3-devkitc-1
platformio run -t uploadfs -e esp32-s3-devkitc-1- Check what's actually on device:
- Monitor serial output shows file listing:
Files in LittleFS: - /web/index.html (8192 bytes)
Symptoms:
ERROR: LittleFS Mount Failed
Attempting to format and remount...
Solutions:
- Erase entire flash:
esptool.py --chip esp32s3 erase_flash- Build everything fresh:
platformio run -t clean -e esp32-s3-devkitc-1
platformio run -t erase -e esp32-s3-devkitc-1
platformio run -t uploadfs -e esp32-s3-devkitc-1
platformio run -t upload -e esp32-s3-devkitc-1Symptoms:
WiFi Connection Failed!
Solutions:
-
Verify credentials in
secret.h:- SSID must match exactly (case-sensitive)
- Password must be correct
-
Check WiFi network availability:
- Ensure 2.4 GHz (not 5 GHz only)
- Check SSID is broadcasting
-
Monitor logs:
platformio run -t monitor -e esp32-s3-devkitc-1
# Look for: "Connecting to WiFi", "WiFi Connected"Symptoms:
MQTT connection failed, rc=...
Solutions:
- Verify WiFi connects first
- Check MQTT server address and port
- Verify credentials (YC_DEVICE_ID, MQTT_PASSWORD)
- Check firewall allows port 8883
platformio run -t buildfs -e esp32-s3-devkitc-1 --verbose 2>&1 | grep -i sizeplatformio device monitor -b 115200 --pattern=.*platformio run -t info -e esp32-s3-devkitc-1Once fully deployed and connected:
-
Open control panel:
- Browser:
http://esp32-watering.local/ - Or:
http://[device_ip]/
- Browser:
-
Firmware update:
- URL:
http://esp32-watering.local/firmware - Username:
admin - Password: (from secret.h OTA_PASSWORD)
- URL:
-
Check status via API:
http://esp32-watering.local/api/statushttp://esp32-watering.local/api/lamp?action=on
# Full deploy (clean β build β upload)
platformio run -t clean -e esp32-s3-devkitc-1 && \
platformio run -t upload -e esp32-s3-devkitc-1 && \
platformio run -t buildfs -e esp32-s3-devkitc-1 && \
platformio run -t uploadfs -e esp32-s3-devkitc-1 && \
platformio device monitor -b 115200 --raw
# Erase and redeploy (nuclear option)
platformio run -t erase -e esp32-s3-devkitc-1 && \
platformio run -t buildfs -e esp32-s3-devkitc-1 && \
platformio run -t uploadfs -e esp32-s3-devkitc-1 && \
platformio run -t upload -e esp32-s3-devkitc-1 && \
platformio device monitor -b 115200 --raw
# Rdeploy (clean β upload)
platformio run -t clean -e esp32-s3-devkitc-1 && \
platformio run -t upload -e esp32-s3-devkitc-1 && \
platformio device monitor -b 115200 --rawExpected signs of success:
- β Serial output readable at 115200 baud
- β WiFi connects automatically
- β MQTT connects
- β
Web interface loads at
http://esp32-watering.local/ - β Can start/stop watering from web UI
- β
Can control plant lamp from web UI (
ON,OFF,AUTO) - β Status updates in real-time
- β Activity log shows commands
- β Learning algorithm adapting watering frequency
After deploying the system for the first time, all valves are uncalibrated. Run the first watering cycle to establish baselines:
mosquitto_pub -h mqtt.cloud.yandex.net -p 8883 --capath /etc/ssl/certs/ \
-u DEVICE_ID -P MQTT_PASSWORD \
-t '$devices/DEVICE_ID/commands' -m 'start_all'- Open
http://esp32-watering.local/ - Use the watering controls for valves
- Use the plant lamp card for
Lamp ON,Lamp OFF, orLamp AUTO
What happens during first watering:
- Each tray fills from "empty" to full
- System records fill time as baseline
- Serial output shows:
π― First watering - Establishing baseline - After completion:
π― BASELINE ESTABLISHED: X.Xs
Send MQTT command:
mosquitto_pub -t '$devices/DEVICE_ID/commands' -m 'learning_status'Serial output shows:
βββββββββββββββββββββββββββββββββββββββββββββ
β LEARNING SYSTEM STATUS β
βββββββββββββββββββββββββββββββββββββββββββββ
π Valve 0:
Status: β Calibrated
Baseline: 30.5s
Last fill: 10.2s
Skip cycles: 2
Total cycles: 5
Last ratio: 0.33 (33% of baseline)
Subscribe to state topic:
mosquitto_sub -t '$devices/DEVICE_ID/state' -vEach valve includes time-based learning data in MQTT state (see "Learning Data Structure" section above for full details).
When a valve is skipped because tray is not empty yet (serial console):
βββββββββββββββββββββββββββββββββββββββ
π§ SMART SKIP: Valve 0
Tray not empty yet (water level: ~45%)
Time since last watering: 12h 0m 0s
Time until empty: 12h 0m 0s
βββββββββββββββββββββββββββββββββββββββ
When learning data is calculated (after successful watering):
π§ TIME-BASED LEARNING:
Fill duration: 4.2s
π Baseline: 5.0s (adaptive)
Water level before: 16%
Tray state was: empty
Estimated empty time: 1d 0h
Learning cycles: 5
β° Auto-watering enabled - will water when empty
When auto-watering triggers:
β° AUTO-WATERING TRIGGERED: Valve 0
Tray is empty - starting automatic watering
Scenario 1: Fast Consumption (Summer)
- Day 1: Fill 5.0s β Baseline: 5.0s, Empty time unknown
- Day 2: Fill 4.8s β Water before: 4%, Empty: 1 day β Auto-waters next day
- Day 3: Fill 5.0s β Water before: 0%, Empty: 1 day β Stable pattern
- System auto-waters daily
Scenario 2: Slow Consumption (Winter)
- Day 1: Fill 5.0s β Baseline: 5.0s
- Day 4: Fill 4.2s β Water before: 16%, Empty: ~4 days
- Day 8: Fill 3.8s β Water before: 24%, Empty: ~4 days β Auto-waters every 4 days
- System adapts to slower consumption
Scenario 3: Different Tray Sizes
- Valve 0: Baseline 2.0s (small tray) β Empty: 12h β Waters twice daily
- Valve 1: Baseline 8.0s (large tray) β Empty: 3 days β Waters every 3 days
- Valve 2: Baseline 5.0s (medium tray) β Empty: 1 day β Waters daily
- Each learns capacity and consumption independently
Replace DEVICE_ID with your actual device ID from secret.h.
Start Sequential Watering:
# Start all valves sequentially (5β0) with Telegram notifications
mosquitto_pub -t '$devices/DEVICE_ID/commands' -m 'start_all'What happens:
- β Waters all 6 trays in sequence (tray 6 β 5 β 4 β 3 β 2 β 1)
- β Sends Telegram start notification with timestamp
- β Tracks duration and status for each tray
- β Sends Telegram completion table when done
- β Updates learning data for each tray
- β Publishes MQTT state every 2 seconds
Individual Valve Control:
- Use the web interface at
http://DEVICE_IP/for manual control - Auto-watering handles trays automatically when empty
- Learning algorithm adapts to each tray independently
Emergency Halt Mode:
# Enter halt mode (blocks all watering operations)
mosquitto_pub -t '$devices/DEVICE_ID/commands' -m 'halt'
# Exit halt mode (resume normal operations)
mosquitto_pub -t '$devices/DEVICE_ID/commands' -m 'resume'Master Overflow Sensor (v1.12.1):
# Reset overflow flag after fixing overflow issue (auto-reinitializes GPIO)
mosquitto_pub -t '$devices/DEVICE_ID/commands' -m 'reset_overflow'
# Or via Telegram: /reset_overflowGPIO Hardware Reinitialization (v1.16.2):
# Force GPIO hardware reinitialization (fixes stuck relays)
mosquitto_pub -t '$devices/DEVICE_ID/commands' -m 'reinit_gpio'
# Or via Telegram: /reinit_gpioNote: /reset_overflow and water level recovery now automatically reinitialize GPIO hardware. Manual /reinit_gpio is useful if relays get stuck without triggering these events.
Via Telegram:
/lampor/lamp_status- show lamp state, mode, GPIO, and schedule/lamp_on- force lamp on manually/lamp_off- force lamp off manually/lamp_auto- return lamp to automatic schedule (22:00 -> 07:00)
Via Web/API:
# Force lamp ON
curl 'http://esp32-watering.local/api/lamp?action=on'
# Force lamp OFF
curl 'http://esp32-watering.local/api/lamp?action=off'
# Return lamp to automatic schedule
curl 'http://esp32-watering.local/api/lamp?action=auto'Behavior:
- Relay is active-low (
PLANT_LIGHT_ACTIVE_HIGH = falseininclude/config.h) - Automatic schedule uses local RTC/system time
- MQTT/web state includes
plant_light.state,plant_light.mode,plant_light.relay_gpio,plant_light.schedule_on,plant_light.schedule_off
Sensor Diagnostics:
# Test all 6 sensors and generate diagnostic report
mosquitto_pub -t '$devices/DEVICE_ID/commands' -m 'test_sensors'
# Test individual sensor (N = 0-5)
mosquitto_pub -t '$devices/DEVICE_ID/commands' -m 'test_sensor_0'
mosquitto_pub -t '$devices/DEVICE_ID/commands' -m 'test_sensor_1'
# ... etcSensor Test Output Example:
βββββββββββββββββββββββββββββββββββββββ
π TESTING ALL 6 SENSORS
βββββββββββββββββββββββββββββββββββββββ
π SENSOR TEST SUMMARY:
Tray | GPIO | Power OFF | Power ON | Status
-----|------|-----------|----------|-------
1 | 8 | HIGH(DRY) | HIGH(DRY) | βοΈ DRY
2 | 9 | HIGH(DRY) | HIGH(DRY) | βοΈ DRY
3 | 10 | HIGH(DRY) | LOW(WET) | π§ WET
4 | 11 | HIGH(DRY) | HIGH(DRY) | βοΈ DRY
5 | 12 | HIGH(DRY) | HIGH(DRY) | βοΈ DRY
6 | 13 | LOW(WET) | LOW(WET) | π§ WET β οΈ <-- HARDWARE FAULT!
Note:
# Subscribe to state updates (includes learning data)
mosquitto_sub -t '$devices/DEVICE_ID/state' -v
# Subscribe to events
mosquitto_sub -t '$devices/DEVICE_ID/events' -v
# Subscribe to all device topics
mosquitto_sub -t '$devices/DEVICE_ID/#' -vEach valve in the state includes a learning object:
{
"pump": "off",
"sequential_mode": false,
"water_level": {
"status": "ok",
"blocked": false
},
"valves": [
{
"id": 0,
"state": "closed",
"phase": "idle",
"rain": false,
"timeout": false,
"learning": {
"calibrated": true,
"auto_watering": true,
"baseline_fill_ms": 5200,
"last_fill_ms": 4200,
"empty_duration_ms": 86400000,
"total_cycles": 5,
"water_level_pct": 45,
"tray_state": "between",
"time_since_watering_ms": 43200000,
"time_until_empty_ms": 43200000,
"last_water_level_pct": 16
}
}
]
}Water Level Sensor Fields (v1.14.0):
water_level.status: Current tank water level - "ok" (water present) or "low" (tank empty)water_level.blocked: Whether watering is blocked due to low water - true (blocked) or false (normal)
Time-Based Learning Fields (v1.5.0):
calibrated: Has the valve completed first baseline calibration?auto_watering: Is automatic watering enabled for this valve?baseline_fill_ms: Time (ms) to fill tray from completely emptylast_fill_ms: Most recent fill time (ms)empty_duration_ms: Learned time for tray to go from full to empty (consumption time)total_cycles: Total successful watering cycles completedwater_level_pct: Current estimated water level (0-100%)tray_state: Current state: "empty", "full", or "between"time_since_watering_ms: Time elapsed since last wateringtime_until_empty_ms: Estimated time until tray is empty (0 if already empty)last_water_level_pct: Water level before last watering
Version: 1.18.2 Platform: ESP32-S3-N8R2 (ESP32-S3-DevKitC-1 compatible) Framework: Arduino + PlatformIO Features: Extracted State Machine, Comprehensive Testing, DS3231 RTC, Water Level Sensor with Smart Delay, Master Overflow Sensor, Emergency Halt Mode, 7-Layer Safety System, Time-Based Learning, Overflow Recovery Protection, Long Outage Detection, Plant Lamp Auto Schedule, Telegram Lamp Control, Web Lamp Dashboard Control Testing: 30+ native tests (no hardware required) New in v1.18.2: Documentation refreshed to match current plant lamp controls, API endpoints, dashboard behavior, and deployment/update workflow