Loading…
No jobs
Loading…
Loading…
Loading…
Option A: Keep existing config (fastest)
UPDATE slaves SET mac = 'new:mac:aa:bb:cc', state = 'pending' WHERE slave_id = 1;
slave.inoOption B: Clean re-create (from dashboard)
Farm log and action log entries are kept either way (they reference slave_id, not MAC).
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.
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.
Meaning: The I2C chip is not responding. All units mapped to GPIO 0–15 (MCP pins) will not work.
Check:
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.
| Virtual Pin | ESP32 GPIO | Status |
|---|---|---|
| 0–15 | MCP23017 | Requires I2C chip |
| 16 | GPIO 0 | ⚠️ Strapping (avoid) |
| 17 | GPIO 1 | ❌ UART TX (kills serial) |
| 18 | GPIO 2 | ✅ Safe |
| 19 | GPIO 3 | ❌ UART RX |
| 20 | GPIO 4 | ✅ Safe |
| 21 | GPIO 5 | ✅ Safe |
| 22–27 | GPIO 6–11 | ⚠️ Flash (input only) |
| 28 | GPIO 12 | ✅ Safe |
| 29 | GPIO 13 | ✅ Safe |
| 30 | GPIO 14 | ✅ Safe |
| 31 | GPIO 15 | ✅ Safe |
| 32 | GPIO 16 | ✅ Safe |
| 33 | GPIO 17 | ✅ Safe |
| 34 | GPIO 18 | ✅ Input only |
| 35 | GPIO 19 | ✅ Input only |
Best for outputs: 18, 20, 28, 29, 30, 31, 32, 33
| Status | Meaning |
|---|---|
| pending | Waiting for master to pick up |
| sent | Master sent via ESP-NOW, waiting for ACK |
| acked | Slave confirmed execution ✓ |
| timeout | Slave didn't respond after 3 retries |
| cancelled | Opposite command was queued (e.g., "off" cancels "on") |
| missed | Timer/duration command was stale when master picked it up |
Old rows (acked/timeout/cancelled/missed) are auto-deleted after 7 days by cleanup cron.
| Frequency | Script | Purpose |
|---|---|---|
| Every minute | timer_check.php | Fire scheduled timers |
| Every minute | duration_check.php | Auto-off expired durations |
| Every 5 min | offline_check.php | Mark offline/dead, clean stale cmds |
| Daily 3 AM | cleanup.php | Delete old logs, optimize tables |
Path: /usr/local/bin/php /home/YOURUSER/public_html/cron/SCRIPT
Check PHP path in cPanel → MultiPHP Manager.
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)
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
Result in Timer list:
| Time | Action | Days |
|---|---|---|
| 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 commands use source='scene' or source='timer' in the action log. The master treats them identically to manual commands — no ESP32 changes needed.
Step 1: Connect the sensor to the ESP32 slave
| Protocol | Wiring | Example |
|---|---|---|
| analog | VCC → 3.3V, GND → GND, AOUT → GPIO 34 or 35 | Capacitive soil moisture |
| digital | VCC → 3.3V, GND → GND, OUT → GPIO (e.g., 2) | Rain detect, flow meter |
| dht22 | VCC → 3.3V, GND → GND, DATA → GPIO (e.g., 18) | Temp + humidity |
| voltage | See "Battery Voltage" below | Battery 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
| Field | What to enter | Example |
|---|---|---|
| Name | Short label (shown in dashboard) | Soil Moist |
| Protocol | How the slave reads it | analog / digital / dht22 / voltage |
| GPIO | Pin number (virtual: 0–35) | 34 |
| Interval | Report every N seconds | 3600 (1 hour) |
| Air / Water | Calibration (analog only) | air=400, water=180 |
Step 3: Calibrate (analog sensors only)
Formula: pct = (air - raw) × 100 / (air - water). Leave blank for no calibration (raw value reported).
Step 4: Save & verify
🔋 Battery Voltage Monitor (10–14V)
Monitor your battery voltage (before the buck converter) to know when to recharge.
Hardware: Voltage Divider (2 resistors)
| Resistor | Value | Position |
|---|---|---|
| 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?
| Battery | V at ADC pin | Safe? |
|---|---|---|
| 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:
| Field | Value |
|---|---|
| Name | Battery |
| Protocol | voltage |
| GPIO | 35 (or 34) |
| Interval | 1800 (30 min) or 3600 (1 hour) |
| Air / Water | Leave blank (not used) |
What you'll see:
| Time | Battery | Meaning |
|---|---|---|
| 14:00 | 12.47 | Normal — no action needed |
| 06:00 | 11.20 | Getting low — plan recharge |
| 04:00 | 10.30 | ⚠️ Low — recharge soon |
| 03:00 | 9.80 | 🔴 Critical — ESP32 may brownout |
Alert thresholds (optional, add to cron):
< 10.5V → "Battery low, recharge soon"< 10.0V → "Battery CRITICAL, brownout risk"Troubleshooting:
💧 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)
| Resistor | Value | Position |
|---|---|---|
| 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 output | V at ADC pin | Safe? |
|---|---|---|
| 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:
| Field | Value |
|---|---|
| Name | Pressure |
| Protocol | analog |
| GPIO | 35 (or 34) |
| Interval | 1800 (30 min) or 3600 (1 hour) |
| Air | Raw ADC at no pressure (e.g., 280) |
| Water | Raw ADC at max pressure (e.g., 3800) |
Calibration (find your Air/Water values):
Formula: pct = (air - raw) × 100 / (air - water). So air=280, water=3800: at raw=280 → 100%, at raw=3800 → 0%.
What you'll see:
| Reading | Meaning | Action |
|---|---|---|
| 80–100% | Normal pressure | None |
| 50–80% | Pressure dropping — check for leak or pump issue | Inspect |
| < 50% | Low pressure — possible leak or empty tank | ⚠️ Investigate |
| 0% | Max pressure or sensor fault | Check wiring |
Troubleshooting:
Adding a new sensor type (firmware change needed):
Adafruit BME280)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);
}
bme280 to the protocol dropdown in dashboard JSchannels = 3, sub_names = "temp,humidity,pressure"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.
The Logs tab → Reset Log card shows every abnormal reset event from the master and slaves.
Example entries:
| Time | Device | Type | Reason |
|---|---|---|---|
| 09-03 05:00 | Master | 🌙 deep sleep | Deep sleep 330 min (wake 05:00) |
| 09-03 05:00 | Slave #1 | 🌙 deep sleep | Deep sleep 330 min (wake 05:00) |
| 09-02 14:22 | Master | 🔴 task WDT | — |
| 09-02 14:22 | Slave #3 | 🔄 software | SW-WDT: stuck 30012ms (reg=1, cfg=1) |
| 09-01 08:15 | Slave #1 | 💥 PANIC | — |
Type legend:
| Icon | Type | Meaning |
|---|---|---|
| 🌙 | deep sleep | Normal scheduled sleep (not an error) |
| 🔴 | task WDT | Hardware watchdog fired — code hung > 15s (I2C stuck, flash corruption, etc.) |
| 🔄 | software | Software WDT fired — loop stuck > 30s (retry loop, logical bug) |
| 💥 | PANIC | ESP32 internal assert failed (stack overflow, heap corruption, null pointer) |
| ⚡ | brownout | Power voltage dropped below minimum (check wiring/battery) |
| 🔄 | power-on | Normal 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:
esp_task_wdt_reset() before that readHow to debug a 💥 PANIC:
https://esp32backtrace.org to decode it → gives file + line numberReset 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).
Shows every slave as a card with:
Server clock line (top of page):
Auto-refreshes every 15 seconds. No action needed — just watch.
Each slave is an expandable card. Inside, each unit has:
How it works:
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.
Schedule units to turn on/off at fixed times each day.
Two modes:
| Mode | Use case | What 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:
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).
Turn a unit ON for a set number of minutes, then it automatically turns OFF.
How to use:
What happens:
Active durations list (below the form):
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.
These settings control the master ESP32's behavior. Changes take effect on the next poll cycle (~10s).
| Setting | Default | Range | What it does |
|---|---|---|---|
| Poll Interval | 10s | 2–300s | How often master checks server for new commands |
| ACK Timeout | 500ms | 100–10000ms | How long to wait for slave response before retry |
| Max Retries | 3 | 0–10 | How many times to re-send if no ACK |
| Retry Interval | 2000ms | 500–30000ms | Wait between retries |
| Sensor Flush | 60s | 10–3600s | How often master sends buffered sensor data to server |
| Offline Timeout | 3 min | 1–60 min | No activity → slave marked "offline" (server-side) |
| Dead Timeout | 25 min | 5–120 min | Offline this long → slave marked "dead" |
| Sleep Start | 22:00 | HH:MM | Master + slaves enter deep sleep |
| Wake Start | 05:00 | HH:MM | Master + slaves wake up |
| Sleep Enabled | ✓ | on/off | Master 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.
Configure each slave's units (relays/motors/MOSFETs) and sensors.
Add a new slave:
slave.ino → power it onUnits (U1–U4):
| Field | Notes |
|---|---|
| Name | Label shown in dashboard (e.g., "durian") |
| Type | motor / relay / mosfet / empty |
| Mode | self-lock (continuous) or latching (pulse) |
| gA | Main GPIO (virtual pin 0–35) |
| gB | Only for latching — opposite pulse pin |
| ms | Pulse 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.
Action Log (last 100 entries):
| Column | Meaning |
|---|---|
| Time | When the action completed (MM-DD + HH:MM) |
| Unit | Unit name + slave/unit ID |
| Action | On / Off / Open / Close / Stop / Scene |
| Min | Actual runtime in minutes (on the OFF event) |
| Source | manual / 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.
A free-form journal for recording farm activities. Supports rich text (bold, italic, underline, bullet lists) and photos.
To add an 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:
Farm log entries are permanent (never auto-deleted). They survive slave deletion.
A scene is a named group of unit actions that run together. Think of it as a "macro."
Example scenes:
To create a scene:
To run a scene:
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.
View historical sensor data as line charts + a recent data table.
How to use:
Chart details:
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.
Three auth layers:
| Who | Key | Where |
|---|---|---|
| ESP32 master/slave | API_TOKEN | config.php + firmware #define |
| Dashboard (browser) | DASHBOARD_KEY | config.php + index.html JS const DK |
| Cron scripts | CRON_KEY | config.php (not enforced for local PHP) |
To rotate keys:
config.php on the server#define API_TOKEN → reflash master + slavesindex.html JS const DK = '...'Dashboard access:
The dashboard requires login at http://yourdomain.com/dashboard/login.html. Only the farm owner (you) has credentials.
Two layers of protection:
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.
Auto-cleanup (daily 3 AM):
| Data | Kept | Why |
|---|---|---|
| Sensor readings | 30 days | Charts max range is 30d |
| Action log | 30 days | Covers a month of operations |
| Status log | 14 days | Diagnostic only |
| Command queue (finished) | 7 days | Acked/timeout/cancelled/missed |
| Active durations (finished) | 7 days | Recent list in Dur tab |
| Farm log | Forever | Your journal — never deleted |
| Reset log | Forever | Diagnostic history |
Backup:
/uploads/farm/ (your photos) via File Manager → Compress → DownloadRestore:
/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.
ESP-NOW range:
WiFi (master → server):
Improving range:
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.
Master power consumption:
| State | Current | Per day |
|---|---|---|
| Active (WiFi + ESP-NOW) | ~180 mA | ~4.3 Wh |
| Deep sleep | ~10–20 µA | ~0.001 Wh |
Slave power consumption:
| State | Current | Notes |
|---|---|---|
| Idle (WiFi + ESP-NOW) | ~80 mA | WiFi is the main draw |
| Solenoid pulse (10ms) | ~200 mA spike | Very brief |
| DHT22 read | ~5 mA extra | ~2s |
| Deep sleep | ~10 µA | ~0.001 Wh/day |
Battery sizing (7-day autonomy, 18h sleep):
⚠️ 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:
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.
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.
slave.ino#define FIRMWARE_VERSION "1.1"slave.ino.bin (or similar)⚠️ During OTA:
Method B: USB Update (first-time / emergency)
Use when: slave is bricked, OTA failed, or first-time setup.
If the slave doesn't enter bootloader mode:
Updating the Master:
The master cannot OTA-update itself (it's the sender). Always update the master via USB.
master.ino in Arduino IDEOrder of updates:
What survives a firmware update:
| Data | Survives? | Where it lives |
|---|---|---|
| Units, sensors, timers, scenes | ✅ Yes | Server database |
| Farm log, action log, reset log | ✅ Yes | Server database |
| Slave's MAC + slave_id | ✅ Yes | Server 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:
| Error | Cause | Fix |
|---|---|---|
| ACK timeout | Slave out of ESP-NOW range | Move closer, retry |
| HTTP 404 | .bin file not on server | Re-upload the file |
| Update.begin failed | Not enough flash space | Check partition scheme is "Minimal SPIFFS (1.9MB APP with OTA)" |
| Update.end failed | Corrupt .bin (wrong file, truncated upload) | Re-export from Arduino IDE, re-upload |
| Slave not in peer table | Slave is dead/offline | Power 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.
"Unit doesn't respond to any command"
"Timer fired but unit didn't move"
"Sensor shows — (no data)"
"Dashboard shows all slaves offline"
"Latching solenoid won't release"
"I changed a setting but nothing happened"