🌿 Farm PianESP-NOW Irrigation Controller

📊Dash
🔘Manual
📅Sched
⏱Dur
⚙️Master
📡Slaves
📋Logs
📝Farm Log
🎬Scenes
📈Sensors
🔧Firmware
❓Help
Add Schedule ▼
: :
Set Duration
Add Scene ▼
Upload Firmware
Available Firmware

Loading…

Update Jobs

No jobs

Slave Versions

Loading…

Master Settings
How often master checks server for new commands How long to wait for slave to respond before retrying How many times to re-send if slave doesn't respond Wait time between each retry attempt How often master sends sensor data to server No activity from slave for this long → marked "offline" Offline for this long → marked "dead" Master + slaves enter deep sleep at this time Master + slaves wake up at this time
Account
Logged in as: …
Add Slave
Action Log last 100 entries

Loading…

Reset Log last 30

Loading…

Add Entry ▼
Manage Types ▼
Replace Broken Slave Hardware ▼

Option A: Keep existing config (fastest)

  1. Note the new slave's MAC (printed on serial boot)
  2. In phpMyAdmin, run:
UPDATE slaves SET mac = 'new:mac:aa:bb:cc', state = 'pending' WHERE slave_id = 1;
  1. Flash new ESP32 with slave.ino
  2. Power on — it auto-registers and gets config
  3. Done. All units, timers, names preserved.

Option B: Clean re-create (from dashboard)

  1. Slaves tab → tap old slave → Delete
  2. Slaves tab → Add → same ID + name
  3. Re-enter unit GPIOs, names, timers
  4. Flash new ESP32 → power on

Farm log and action log entries are kept either way (they reference slave_id, not MAC).

Slave Shows "Dead" But Is Actually Alive ▼

Cause: Master was powered off. Server's last_seen stopped updating → offline_timeout_min (default 3) min = offline → dead_timeout_min (default 25) min = dead. Both configurable in Master tab.

Fix: Restart the master. It will re-register the slave → server updates to "online" automatically.

Prevent: The master heartbeat (every 5 min) reports alive slaves to the server. If the dot next to "server" time is 🔴, the master is down.

Timer Fired But Nothing Happened ▼

Check 1: Is the master online? (🟢 dot in header)

Check 2: Check Logs tab — look for ⚠ "missed" or ✗ "timeout" entries.

Check 3: Check cron is running (cPanel → Cron Jobs → should show timer_check every minute).

Check 4: In phpMyAdmin, verify timer_schedules.days includes today's number (1=Mon … 7=Sun).

If master was down when timer fired, the command is marked "missed" after 10 min. It will NOT execute late.

MCP23017 NACK (err=4) on Slave Boot ▼

Meaning: The I2C chip is not responding. All units mapped to GPIO 0–15 (MCP pins) will not work.

Check:

  • VCC = 3.3V (not 5V)
  • SDA → GPIO 21, SCL → GPIO 22
  • 4.7 kΩ pull-up resistors from SDA/SCL to 3.3V
  • ADDR pin (pin 19) grounded for address 0x20

Workaround: Remap all units to native ESP32 pins (virtual pin 16+) in Slaves tab. MCP is bypassed entirely. See "Safe GPIO Pins" below for which pins to use.

Safe GPIO Pins (ESP32) ▼
Virtual PinESP32 GPIOStatus
0–15MCP23017Requires I2C chip
16GPIO 0⚠️ Strapping (avoid)
17GPIO 1❌ UART TX (kills serial)
18GPIO 2✅ Safe
19GPIO 3❌ UART RX
20GPIO 4✅ Safe
21GPIO 5✅ Safe
22–27GPIO 6–11⚠️ Flash (input only)
28GPIO 12✅ Safe
29GPIO 13✅ Safe
30GPIO 14✅ Safe
31GPIO 15✅ Safe
32GPIO 16✅ Safe
33GPIO 17✅ Safe
34GPIO 18✅ Input only
35GPIO 19✅ Input only

Best for outputs: 18, 20, 28, 29, 30, 31, 32, 33

Command Queue Status Explained ▼
StatusMeaning
pendingWaiting for master to pick up
sentMaster sent via ESP-NOW, waiting for ACK
ackedSlave confirmed execution ✓
timeoutSlave didn't respond after 3 retries
cancelledOpposite command was queued (e.g., "off" cancels "on")
missedTimer/duration command was stale when master picked it up

Old rows (acked/timeout/cancelled/missed) are auto-deleted after 7 days by cleanup cron.

Cron Jobs (must be set in cPanel) ▼
FrequencyScriptPurpose
Every minutetimer_check.phpFire scheduled timers
Every minuteduration_check.phpAuto-off expired durations
Every 5 minoffline_check.phpMark offline/dead, clean stale cmds
Daily 3 AMcleanup.phpDelete old logs, optimize tables

Path: /usr/local/bin/php /home/YOURUSER/public_html/cron/SCRIPT
Check PHP path in cPanel → MultiPHP Manager.

Scene Timer — On/Off Scheduling ▼

What is a scene?

A scene is a named group of unit actions. One tap (or one timer) runs all of them together.

Step 1: Create scenes (Scenes tab)

  1. Open Scenes tab → tap "Add Scene"
  2. Enter a name (e.g. "Morning Open") and pick an icon
  3. Tap + Add Action for each unit:
  • Select slave → unit → action (on/off/open/close/stop)
  • Optional: set a duration in minutes (for "on" actions)
  1. Tap Save Scene

Step 2: Create the "off" scene

Create a second scene with the opposite actions (e.g. "Midday Close": durian close, rambutan close, veg1 off).

Step 3: Schedule in Timer tab

  1. Open Timer tab → "Add Time Slot"
  2. Change Mode to 🎬 Scene
  3. Select the scene (e.g. "Morning Open")
  4. Set the time (e.g. 06:00) and days
  5. Tap Add Slot
  6. Repeat for the "off" scene (e.g. "Midday Close" at 13:00)

Result in Timer list:

TimeActionDays
06:00🌅 Morning Open (scene)M T W T F S S
13:00🌙 Midday Close (scene)M T W T F S S

Manual run (no timer):

Scenes tab → tap ▶ Run on any scene card. All actions queue immediately (master picks up within ~10s).

Troubleshooting:

  • Scene timer fired but nothing happened → check master is online (🟢 in header)
  • Scene ran but only some units moved → check GPIO mapping in Slaves tab
  • To stop a scene mid-run → use Manual tab to send "off"/"close" to each unit, or create a "Stop All" scene

Scene commands use source='scene' or source='timer' in the action log. The master treats them identically to manual commands — no ESP32 changes needed.

Adding a New Sensor ▼

Step 1: Connect the sensor to the ESP32 slave

ProtocolWiringExample
analogVCC → 3.3V, GND → GND, AOUT → GPIO 34 or 35Capacitive soil moisture
digitalVCC → 3.3V, GND → GND, OUT → GPIO (e.g., 2)Rain detect, flow meter
dht22VCC → 3.3V, GND → GND, DATA → GPIO (e.g., 18)Temp + humidity
voltageSee "Battery Voltage" belowBattery level (10–14V)

Use safe GPIO pins (see "Safe GPIO Pins" above). Analog pins: only 34 or 35 (GPIO 18/19 = ADC2). MCP pins (0–15) work for digital only. DHT22 works on any digital output pin (e.g., 18, 20, 28–33).

Step 2: Configure in Slaves tab

  1. Open Slaves tab → expand the slave card
  2. Scroll to Sensors section → tap + Add Sensor
  3. Fill in:
FieldWhat to enterExample
NameShort label (shown in dashboard)Soil Moist
ProtocolHow the slave reads itanalog / digital / dht22 / voltage
GPIOPin number (virtual: 0–35)34
IntervalReport every N seconds3600 (1 hour)
Air / WaterCalibration (analog only)air=400, water=180

Step 3: Calibrate (analog sensors only)

  1. Insert probe in air → open Serial Monitor (115200) → note the raw value → enter as air
  2. Submerge probe in water → note the raw value → enter as water
  3. Save → slave now reports 0–100% instead of raw 0–1023

Formula: pct = (air - raw) × 100 / (air - water). Leave blank for no calibration (raw value reported).

Step 4: Save & verify

  1. Tap Save on the slave card
  2. Wait ~10s (master pushes config to slave)
  3. Check Dashboard → expand "▼ Sensors" → data should appear
  4. Check Sensors tab → select slave → graph appears

🔋 Battery Voltage Monitor (10–14V)

Monitor your battery voltage (before the buck converter) to know when to recharge.

Hardware: Voltage Divider (2 resistors)

ResistorValuePosition
R1 (top)39kΩ (¼W, 5%)Battery(+) → middle
R2 (bottom)10kΩ (¼W, 5%)Middle → GND

Wiring diagram:

	Battery (+) ────[39kΩ]────┬──── GPIO 35 (virtual pin 35)
							  │
						   [10kΩ]
							  │
							 GND

	Battery (+) also connects to Buck converter input
	

Why 39k / 10k?

BatteryV at ADC pinSafe?
14V (max)2.86V✅ (< 3.0V limit)
12V (nominal)2.45V✅
10V (min)2.04V✅
16V (spike)3.27V⚠️ (under 3.3V but close)

Current draw from battery: 14V ÷ 49kΩ = 0.29 mA (negligible). Cost: ~$0.10 for both resistors.

Dashboard configuration:

FieldValue
NameBattery
Protocolvoltage
GPIO35 (or 34)
Interval1800 (30 min) or 3600 (1 hour)
Air / WaterLeave blank (not used)

What you'll see:

TimeBatteryMeaning
14:0012.47Normal — no action needed
06:0011.20Getting low — plan recharge
04:0010.30⚠️ Low — recharge soon
03:009.80🔴 Critical — ESP32 may brownout

Alert thresholds (optional, add to cron):

  • < 10.5V → "Battery low, recharge soon"
  • < 10.0V → "Battery CRITICAL, brownout risk"

Troubleshooting:

  • Shows 0 or very low → check R1 is connected to Battery(+), not GND
  • Shows 3.3 or 4095 → R2 is missing or GPIO is floating (not connected to divider midpoint)
  • Readings jitter ±0.1V → add a 100nF capacitor across R2 (from GPIO to GND)
  • Shows -1 → wrong GPIO (must be 34 or 35 for ADC) or GPIO < 16 (MCP has no ADC)

💧 Water Pressure Sensor (Analog, 5V)

Monitor pipe/tank water pressure. Uses the standard analog protocol — no firmware change needed.

⚠️ Voltage divider required (sensor outputs 0–5V, ESP32 max 3.3V)

ResistorValuePosition
R1 (top)22kΩ (¼W, 5%)Sensor OUT → middle
R2 (bottom)33kΩ (¼W, 5%)Middle → GND

Wiring diagram:

Sensor VDD ──────────────────────────→ 5V
Sensor GND ──────────────────────────→ GND
Sensor OUT ──[22kΩ]──┬── GPIO 35 (virtual pin 35)
                     │
                  [33kΩ]
                     │
                    GND

Why 22k / 33k?

Sensor outputV at ADC pinSafe?
0V (no pressure)0V✅
3.3V (mid range)2.0V✅
5V (max pressure)3.0V✅ (< 3.3V limit)

Current draw: 5V ÷ 55kΩ = 0.09 mA (negligible). Cost: ~$0.10 for both resistors.

Dashboard configuration:

FieldValue
NamePressure
Protocolanalog
GPIO35 (or 34)
Interval1800 (30 min) or 3600 (1 hour)
AirRaw ADC at no pressure (e.g., 280)
WaterRaw ADC at max pressure (e.g., 3800)

Calibration (find your Air/Water values):

  1. No pressure: Connect sensor with pipe open (atmospheric). Set interval to 5s temporarily. Check dashboard → note the raw value (e.g., 280). Enter as Air.
  2. Max pressure: Turn on pump / apply full pressure. Note the raw value (e.g., 3800). Enter as Water.
  3. Save → dashboard now shows 0–100% (100% = no pressure, 0% = max pressure)
  4. Set interval back to 1800 or 3600

Formula: pct = (air - raw) × 100 / (air - water). So air=280, water=3800: at raw=280 → 100%, at raw=3800 → 0%.

What you'll see:

ReadingMeaningAction
80–100%Normal pressureNone
50–80%Pressure dropping — check for leak or pump issueInspect
< 50%Low pressure — possible leak or empty tank⚠️ Investigate
0%Max pressure or sensor faultCheck wiring

Troubleshooting:

  • Shows 100% always → sensor not powered (check 5V) or OUT pin not connected to divider
  • Shows 0% always → R2 missing or GPIO floating
  • Readings jump erratically → add 100nF capacitor across R2 (GPIO to GND)
  • Shows -1 → wrong GPIO (must be 34 or 35) or GPIO < 16
  • Slow drift over hours → normal (temperature affects piezoresistive sensors)

Adding a new sensor type (firmware change needed):

  1. Install the sensor's Arduino library (e.g. Adafruit BME280)
  2. In slave firmware checkSensors(), add a new case:
    else if (strcmp(sensors[j].protocol, "bme280") == 0) {
    	  float t = bme.readTemperature();
    	  float h = bme.readHumidity();
    	  float p = bme.readPressure() / 100.0;
    	  arr.add(round(t * 10.0) / 10.0);
    	  arr.add(round(h * 10.0) / 10.0);
    	  arr.add(round(p * 10.0) / 10.0);
    	}
  3. Add bme280 to the protocol dropdown in dashboard JS
  4. Set channels = 3, sub_names = "temp,humidity,pressure"
  5. Flash slave → Save in Slaves tab → done

No master firmware change needed for new sensors — the config push format is generic (name, protocol, channels, sub_names, gpio, gpio2, interval, config JSON). The master just forwards the sensor definition to the slave.

Reset Log — What You'll See ▼

The Logs tab → Reset Log card shows every abnormal reset event from the master and slaves.

Example entries:

TimeDeviceTypeReason
09-03 05:00Master🌙 deep sleepDeep sleep 330 min (wake 05:00)
09-03 05:00Slave #1🌙 deep sleepDeep sleep 330 min (wake 05:00)
09-02 14:22Master🔴 task WDT—
09-02 14:22Slave #3🔄 softwareSW-WDT: stuck 30012ms (reg=1, cfg=1)
09-01 08:15Slave #1💥 PANIC—

Type legend:

IconTypeMeaning
🌙deep sleepNormal scheduled sleep (not an error)
🔴task WDTHardware watchdog fired — code hung > 15s (I2C stuck, flash corruption, etc.)
🔄softwareSoftware WDT fired — loop stuck > 30s (retry loop, logical bug)
💥PANICESP32 internal assert failed (stack overflow, heap corruption, null pointer)
⚡brownoutPower voltage dropped below minimum (check wiring/battery)
🔄power-onNormal boot (never logged — only shown on serial)

Why "—" for some entries?

When the hardware watchdog (🔴) or PANIC (💥) fires, the chip dies instantly — it cannot write a reason to NVS before death. On the next boot, the firmware reads esp_reset_reason() to identify the type, but the specific cause (which line of code, which I2C transaction) is lost. The "—" means "type known, detail unavailable."

By contrast, the software WDT (🔄) has time to write a detailed reason (e.g., "stuck 30012ms, lastPoll=45230") before calling esp_restart() — that's why it has a full description.

How to debug a 🔴 task WDT:

  1. Check the slave's serial output just before the reset (last line printed)
  2. If no serial output → I2C bus hang (MCP23017) or DHT22 stuck. Check wiring + pull-ups
  3. If it happens at the same time daily → a sensor read is blocking. Add esp_task_wdt_reset() before that read
  4. If random → possible brownout misidentified (check voltage), or flash wear (rare)

How to debug a 💥 PANIC:

  1. The serial shows a backtrace (hex addresses) before the reset
  2. Use https://esp32backtrace.org to decode it → gives file + line number
  3. Common causes: stack overflow (large local arrays), buffer overflow (strncpy too short), null pointer dereference

Reset logs are stored on the server (not on the ESP32). They survive power loss and are visible in the dashboard even when the device is offline. Old entries are kept indefinitely (no auto-cleanup).

📊 Dashboard Tab — At-a-Glance Status ▼

Shows every slave as a card with:

  • Status badge — Online (green) / Offline (gray) / Dead (red) / Pending (amber)
  • Unit dots — green glow = on, gray = off, amber pulse = command in flight, red = slave dead
  • End time — if a unit is on via timer or duration, shows the HH:MM it will turn off
  • ▼ Sensors — tap to expand last 10 sensor readings for that slave

Server clock line (top of page):

  • ⏱ shows the server's current time (Thailand)
  • 🟢 = master heartbeat received < 10 min ago (master is alive)
  • 🔴 = master heartbeat > 10 min ago (master is down or WiFi lost)

Auto-refreshes every 15 seconds. No action needed — just watch.

🔘 Manual Tab — Toggle Units On/Off ▼

Each slave is an expandable card. Inside, each unit has:

  • Toggle switch — tap to send ON or OFF (or OPEN/CLOSE for motors). The switch turns amber (pending) while waiting for the slave's ACK.
  • STOP button — immediately stops the unit (turns off a running duration, releases a latching solenoid). Use this as an emergency stop.

How it works:

  1. You tap the toggle → command is queued on the server
  2. Master picks it up within ~10s (poll interval)
  3. Master sends via ESP-NOW → slave executes → slave ACKs
  4. Dashboard updates (green dot, switch snaps to new position)

Total latency: ~10–15s from tap to visual confirmation. If the master is down (🔴), the command stays "pending" until the master comes back.

Tip: If you tap ON then quickly tap OFF, the ON is automatically cancelled (server-side dedup). Only the last command executes.

📅 Sched Tab — Daily Timers ▼

Schedule units to turn on/off at fixed times each day.

Two modes:

ModeUse caseWhat it creates
⏱ Time pair"Open durian 06:00, close 13:00"2 rows (on + off) sharing a group
🎬 Scene"Run Morning Watering scene at 06:00"1 row referencing a scene

Days: Check the days you want it to run (Mon–Sun). Default = every day.

Managing existing timers:

  • Disable/Enable — pauses the timer without deleting it (grayed out in list)
  • Edit — change times or days (both on/off times change together)
  • ✕ — delete the entire pair (or scene timer)

Timers fire via the server cron (every minute). If the master is in deep sleep, timers are skipped (they won't fire late after wake).

⏱ Dur Tab — Timed Watering ▼

Turn a unit ON for a set number of minutes, then it automatically turns OFF.

How to use:

  1. Select slave + unit
  2. Enter duration in minutes (1–1440)
  3. Tap Start

What happens:

  • Slave turns the unit ON immediately
  • Slave's internal timer counts down (auto-off, no network needed)
  • Server also tracks the end time (backup if slave misses it)
  • Dashboard shows the countdown end time on the unit dot

Active durations list (below the form):

  • Stop — sends an immediate OFF to the slave (cancels the timer)
  • Cancel — removes from server tracking (use if the slave already turned off but the server hasn't updated yet)

If the slave loses power mid-duration, the unit stays in its last physical state (GPIO resets to input on ESP32 reboot). The server will mark it as expired after the timeout.

⚙️ Master Tab — Global Settings ▼

These settings control the master ESP32's behavior. Changes take effect on the next poll cycle (~10s).

SettingDefaultRangeWhat it does
Poll Interval10s2–300sHow often master checks server for new commands
ACK Timeout500ms100–10000msHow long to wait for slave response before retry
Max Retries30–10How many times to re-send if no ACK
Retry Interval2000ms500–30000msWait between retries
Sensor Flush60s10–3600sHow often master sends buffered sensor data to server
Offline Timeout3 min1–60 minNo activity → slave marked "offline" (server-side)
Dead Timeout25 min5–120 minOffline this long → slave marked "dead"
Sleep Start22:00HH:MMMaster + slaves enter deep sleep
Wake Start05:00HH:MMMaster + slaves wake up
Sleep Enabled✓on/offMaster switch for deep sleep

Per-slave sleep override: go to Slaves tab → expand a slave → set its own Sleep/Wake times. The per-slave setting takes priority over the global one.

📡 Slaves Tab — Hardware Config ▼

Configure each slave's units (relays/motors/MOSFETs) and sensors.

Add a new slave:

  1. Enter an ID (1–20) + name at the top
  2. Tap Add → slave appears in the list with "Pending" badge
  3. Flash the ESP32 with slave.ino → power it on
  4. It auto-registers (MAC assigned) → badge changes to "Online"
  5. Expand the card → configure units + sensors → tap Save
  6. Master pushes config to slave within ~10s

Units (U1–U4):

FieldNotes
NameLabel shown in dashboard (e.g., "durian")
Typemotor / relay / mosfet / empty
Modeself-lock (continuous) or latching (pulse)
gAMain GPIO (virtual pin 0–35)
gBOnly for latching — opposite pulse pin
msPulse width for latching (e.g., 10ms for solenoid)

Sensors:

Tap + Add Sensor for each. Configure name, protocol, GPIO, interval, and calibration (analog only). See "Adding a New Sensor" above for details.

Delete a slave: Tap the red Delete button. This removes all units, sensors, timers, and sensor history for that slave. Farm log and action log entries are kept.

Changes are pushed to the slave automatically after Save. The slave's config version increments, so it accepts the new push.

📋 Logs Tab — Action History + Reset Log ▼

Action Log (last 100 entries):

ColumnMeaning
TimeWhen the action completed (MM-DD + HH:MM)
UnitUnit name + slave/unit ID
ActionOn / Off / Open / Close / Stop / Scene
MinActual runtime in minutes (on the OFF event)
Sourcemanual / timer / duration / scene
Result✓ ack (green) / ✗ timeout (red) / ⚠ missed (amber)

Reset Log (last 30 entries):

Shows every abnormal ESP32 reset (WDT, PANIC, deep sleep, brownout). See "Reset Log — What You'll See" above for the full explanation.

Action log is kept 30 days. Reset log is kept indefinitely.

📝 Farm Log Tab — Personal Journal ▼

A free-form journal for recording farm activities. Supports rich text (bold, italic, underline, bullet lists) and photos.

To add an entry:

  1. Set the date/time (defaults to now)
  2. Select a type (Watering, Pest Control, Weather, etc.)
  3. Type your note. Use the toolbar for formatting.
  4. Tap 📷 to attach a photo (max 5MB, JPG/PNG/WebP)
  5. Tap Save Entry

Managing types:

Expand "Manage Types" at the bottom to add/rename/delete categories. You can't delete a type that has entries.

Reading entries:

  • Long entries are clamped (show "▼ tap to expand")
  • Photos show as 📷 badges when collapsed — tap to expand to see them
  • Tap Edit to modify an entry, ✕ to delete
  • Tap Load More to page through older entries (50 per page)

Farm log entries are permanent (never auto-deleted). They survive slave deletion.

🎬 Scenes Tab — Multi-Unit Bundles ▼

A scene is a named group of unit actions that run together. Think of it as a "macro."

Example scenes:

  • 🌅 "Morning Open" — durian open, rambutan open, veg1 on (all at once)
  • 🌙 "Midday Close" — durian close, rambutan close, veg1 off
  • 💧 "Soak All" — veg1 on 30min, veg2 on 30min (with 20s delay between)

To create a scene:

  1. Tap "Add Scene"
  2. Enter name + pick an icon
  3. Tap + Add Action for each unit
  4. Set the action (on/off/open/close/stop) + optional duration (min) + optional delay (s)
  5. Tap Save Scene

To run a scene:

  • Tap ▶ Run on the scene card → all actions queue immediately
  • Or schedule it in the Sched tab (Scene mode) → fires at the set time daily

Delay (stagger):

Set a delay in seconds on individual actions to stagger solenoid activation (avoids current spike). Max practical delay: ~4 minutes (server's 5-min stale filter).

Scene commands are marked with source='scene' in the Action Log. If you delete a scene, its scheduled timers are also deleted.

📈 Sensors Tab — Charts & Data ▼

View historical sensor data as line charts + a recent data table.

How to use:

  1. Select a Slave from the dropdown
  2. Select a Range: 24h, 7d, or 30d
  3. Charts appear (one per sensor, auto-scaled)
  4. Below the charts: "Recent Data" table (last 24 readings)

Chart details:

  • Multi-channel sensors (e.g., DHT22) show 2 lines (temp + humidity) with a legend
  • Hover/tap a point to see the exact value (tooltip)
  • Points disappear when > 50 data points (line only, for performance)
  • "—" in the table means the reading failed (sensor error, shown as -1)

Data retention: 30 days. Older data is deleted by the nightly cleanup cron.

Sensor data is reported at the interval you set in Slaves tab (default: 1 hour). For 24h view you'll see ~24 points; for 30d view ~720 points.

🔒 Security & Access ▼

Three auth layers:

WhoKeyWhere
ESP32 master/slaveAPI_TOKENconfig.php + firmware #define
Dashboard (browser)DASHBOARD_KEYconfig.php + index.html JS const DK
Cron scriptsCRON_KEYconfig.php (not enforced for local PHP)

To rotate keys:

  1. Generate a new random string (e.g., 32+ chars)
  2. Update config.php on the server
  3. Update the firmware #define API_TOKEN → reflash master + slaves
  4. Update index.html JS const DK = '...'
  5. Clear browser cache (or hard-refresh) to load the new key

Dashboard access:

The dashboard requires login at http://yourdomain.com/dashboard/login.html. Only the farm owner (you) has credentials.

  • Session timeout: 30 minutes of inactivity → auto-logout
  • Change password: Master tab → Account section at the bottom
  • Logout: red "Logout" button in the same section

Two layers of protection:

  1. Login page (session) — protects the dashboard UI
  2. Dashboard key (dk) — protects the API endpoints (even without login, API calls need the key)

The API endpoints (/api/*.php) are also accessible if someone knows the URL + token. Don't share your config.php.

💾 Data Retention & Backup ▼

Auto-cleanup (daily 3 AM):

DataKeptWhy
Sensor readings30 daysCharts max range is 30d
Action log30 daysCovers a month of operations
Status log14 daysDiagnostic only
Command queue (finished)7 daysAcked/timeout/cancelled/missed
Active durations (finished)7 daysRecent list in Dur tab
Farm logForeverYour journal — never deleted
Reset logForeverDiagnostic history

Backup:

  1. cPanel → phpMyAdmin → select your database → Export (Custom: all tables, SQL format)
  2. Save the .sql file to your phone/PC
  3. Also backup /uploads/farm/ (your photos) via File Manager → Compress → Download
  4. Recommended: weekly export (set a phone reminder)

Restore:

  1. phpMyAdmin → select database → Import → upload .sql file
  2. Upload photos back to /uploads/farm/

A full backup (DB + photos) is typically < 50 MB. You can also set up cPanel's auto-backup feature (if your host allows) to include the database in daily/weekly server backups.

📶 Network & Range ▼

ESP-NOW range:

  • Open field: ~100–300 m (depends on antenna, obstacles, channel)
  • Through trees/foliage: ~30–80 m (RF attenuation from moisture)
  • Through concrete/metal: unreliable

WiFi (master → server):

  • Master must be within range of your WiFi router
  • Slaves also need WiFi (for NTP sync), but can be farther from router than master
  • If a slave loses WiFi, it still works (ESP-NOW doesn't need internet) but loses NTP → sleep/wake stops

Improving range:

  1. Use the ESP32's onboard PCB antenna (don't cover it with metal/enclosure)
  2. Keep master and slaves on the same WiFi channel (auto-negotiated, but verify in serial: "Ch: 1")
  3. Elevate the master antenna (roof, pole) for best coverage
  4. Avoid placing ESP32 next to metal water tanks, batteries, or solenoid coils
  5. If range is insufficient: add a WiFi repeater/mesh node mid-farm (master connects to repeater)

WiFi channel note:

ESP-NOW operates on the same channel as your WiFi connection. If your router auto-switches channels (e.g., from 1 to 6), the master and slaves will follow automatically (they reconnect). No action needed.

If you're getting frequent "TX fail" messages on serial, the slave is out of range or obstructed. Move it closer or add a repeater.

⚡ Power & Battery ▼

Master power consumption:

StateCurrentPer day
Active (WiFi + ESP-NOW)~180 mA~4.3 Wh
Deep sleep~10–20 µA~0.001 Wh

Slave power consumption:

StateCurrentNotes
Idle (WiFi + ESP-NOW)~80 mAWiFi is the main draw
Solenoid pulse (10ms)~200 mA spikeVery brief
DHT22 read~5 mA extra~2s
Deep sleep~10 µA~0.001 Wh/day

Battery sizing (7-day autonomy, 18h sleep):

  • Master: ~6.5 Wh active + ~0.002 Wh sleep ≈ **7 Wh** → 2000 mAh 3.7V LiPo (7.4 Wh) with margin
  • Slave: ~3 Wh active + ~0.001 Wh sleep ≈ **3 Wh** → 1000 mAh 3.7V LiPo (3.7 Wh) with margin

⚠️ WiFi is the power killer. If you don't need NTP/sleep on a slave, you can disable WiFi (`WiFi.mode(WIFI_NULL)`) and save ~70 mA. The slave will still work via ESP-NOW but won't sync time or enter deep sleep.

Solar option:

  • 5W panel + 2200 mAh LiPo + TP4056 charger → runs master indefinitely in tropical sun
  • For slaves: 2W panel + 1000 mAh is usually sufficient (especially with 18h sleep)

If you see "BROWNOUT" in the Reset Log, your battery is too small or the solenoid inrush current is dropping the voltage. Add a 470µF capacitor across the solenoid power rails.

🔧 Firmware Update — Step by Step ▼

Two methods: OTA (normal, no cable) or USB (first-time / emergency).

Method A: OTA Update (recommended)

No cable needed. Works for slaves out of WiFi range.

  1. Build the firmware:
    • Open Arduino IDE → open slave.ino
    • Increment version: #define FIRMWARE_VERSION "1.1"
    • Tools → Board → ESP32 Dev Module
    • Tools → Partition Scheme → Minimal SPIFFS (1.9MB APP with OTA)
    • Tools → Flash Size → 4MB (32Mb)
    • Sketch → Export Compiled Binary
    • A file appears in your Downloads: slave.ino.bin (or similar)
  2. Upload to server:
    • Open Dashboard → Firmware tab
    • Enter version (e.g., "1.1")
    • Select the .bin file
    • Tap Upload
  3. Start the update:
    • In "Available Firmware" list, tap All Slaves (or select a specific slave)
    • Confirm the dialog
  4. Watch progress:
    • "Update Jobs" section shows each slave's progress bar
    • Each slave takes ~15–30 seconds (sequential, one at a time)
    • 20 slaves ≈ 5–10 minutes total
    • ✅ = success, ❌ = failed (check error message)
  5. Verify:
    • After ~20 seconds, each slave reboots and re-registers
    • "Slave Versions" card at the bottom shows all slaves on the new version
    • Dashboard tab shows the new version tag (e.g., "v1.1") on each slave card

⚠️ During OTA:

  • Do NOT power off the slave mid-transfer (dual-partition protects against this, but it wastes a cycle)
  • The master is busy during transfer — normal commands are paused until OTA completes
  • If a slave is out of ESP-NOW range, it will show ❌ "ACK timeout" — move it closer and retry

Method B: USB Update (first-time / emergency)

Use when: slave is bricked, OTA failed, or first-time setup.

  1. Connect: USB cable from ESP32 to computer
  2. Arduino IDE:
    • Tools → Board → ESP32 Dev Module
    • Tools → Port → select the COM port (e.g., COM5 or /dev/ttyUSB0)
    • Tools → Partition Scheme → Minimal SPIFFS (1.9MB APP with OTA)
    • Tools → Flash Size → 4MB (32Mb)
  3. Upload: Click the → (upload) button. Wait for "Hard resetting via RTS pin…"
  4. Done: Unplug USB, reconnect power. Slave boots, re-registers, gets config. ~10–20 seconds.

If the slave doesn't enter bootloader mode:

  • Hold BUTTON (GPIO 0) while plugging in USB → release after 2s
  • Or: hold BUTTON, tap RESET, release BUTTON
  • Serial monitor should show "Connecting..." instead of your normal boot log

Updating the Master:

The master cannot OTA-update itself (it's the sender). Always update the master via USB.

  1. Open master.ino in Arduino IDE
  2. Same board settings as above
  3. Upload → done

Order of updates:

  1. Master first (it has the OTA sender code + any protocol changes)
  2. Then slaves (via OTA or USB)

What survives a firmware update:

DataSurvives?Where it lives
Units, sensors, timers, scenes✅ YesServer database
Farm log, action log, reset log✅ YesServer database
Slave's MAC + slave_id✅ YesServer database
Slave's NVS config cache❌ No (wiped)Re-syncs from server in ~10s
Master's NVS peer table❌ No (wiped)Re-syncs from server in ~10s

Troubleshooting OTA failures:

ErrorCauseFix
ACK timeoutSlave out of ESP-NOW rangeMove closer, retry
HTTP 404.bin file not on serverRe-upload the file
Update.begin failedNot enough flash spaceCheck partition scheme is "Minimal SPIFFS (1.9MB APP with OTA)"
Update.end failedCorrupt .bin (wrong file, truncated upload)Re-export from Arduino IDE, re-upload
Slave not in peer tableSlave is dead/offlinePower on the slave, wait for it to register, retry

The "Slave Versions" card in the Firmware tab shows which slaves are on which version. Once all show the same version with [latest] badge, you're done.

🧪 Troubleshooting Checklist ▼

"Unit doesn't respond to any command"

  1. Check Dashboard — is the slave Online? (If Dead → see "Slave Shows Dead" above)
  2. Check Serial Monitor on slave — is it printing "[SLAVE] RX ACK"? If not, ESP-NOW isn't reaching it
  3. Check GPIO wiring — is the relay/solenoid actually connected to the right pin?
  4. Check Slaves tab — is the unit configured with the correct GPIO?
  5. Check Logs tab — is the result "timeout"? (slave received but didn't ACK → firmware bug) or "missed"? (master never sent it)

"Timer fired but unit didn't move"

  1. Was the master online at that time? (Check Reset Log — was it in deep sleep?)
  2. Check Logs tab — is there a "missed" entry? (master was down)
  3. Check Sched tab — is the timer still active? (not disabled)
  4. Check the day — does the timer's day checkboxes include today?

"Sensor shows — (no data)"

  1. Is the sensor configured in Slaves tab? (correct GPIO, protocol)
  2. Is the GPIO correct? (Analog must be 34 or 35 only)
  3. Check Serial Monitor on slave — is it printing sensor values?
  4. Wait one full report interval (default 1 hour) after saving config

"Dashboard shows all slaves offline"

  1. Check the 🟢/🔴 dot next to server time — is the master down?
  2. Check master's WiFi — is the router on? Is the master within range?
  3. Check master's Serial — is it printing "Heartbeat OK" every 5 min?
  4. If master is up but slaves are offline: check ESP-NOW range (see "Network & Range")

"Latching solenoid won't release"

  1. Was gpioB configured? (Slaves tab → expand slave → check U1 has gB filled in)
  2. Is the pulse_ms set? (should be 10–20 for most solenoids)
  3. Check Serial — does it print "U1 CLOSE (latch)" when you tap OFF?
  4. If yes but no physical click: check gpioB wiring (is the second solenoid coil connected?)
  5. Was the firmware updated to include the gpioB init fix? (Bug #2 from the review)

"I changed a setting but nothing happened"

  1. Master settings: take effect on next poll (~10s). Check serial for the new values
  2. Slave settings (units/sensors): take effect after config push (~10s after Save). Check serial for "Config vN loaded"
  3. Timer changes: take effect immediately (cron reads DB every minute)
  4. If still not working: hard-refresh the browser (Ctrl+Shift+R) to clear cached JS