5 Commits

Author SHA1 Message Date
a46604d970 Updated cmds 2025-07-22 16:28:21 -06:00
1cd35970c1 Added mqtt docs 2025-07-20 12:05:58 -06:00
d4fb078a40 Updated after completion of scheduler 2025-07-20 12:05:16 -06:00
9ca88df1cb Added project plan 2025-07-20 12:04:32 -06:00
ba43d22a1a Added Scheduler 2025-07-20 11:52:00 -06:00
8 changed files with 2404 additions and 32 deletions

413
MQTT_README.md Normal file
View File

@ -0,0 +1,413 @@
# MQTT Topics Reference
Complete reference for all MQTT topics used by the ESP32-S3 Plant Watering System.
## Topic Structure Overview
```
plant_watering/
├── status # System online/offline status
├── pump/ # Pump control and monitoring
│ ├── 1/ # Pump 1
│ │ ├── set # Control commands
│ │ ├── state # Current state
│ │ ├── speed # Speed control
│ │ ├── stats # Runtime statistics
│ │ ├── runtime # Current runtime (when running)
│ │ └── cooldown # Cooldown status
│ └── 2/ # Pump 2 (same structure)
├── moisture/ # Moisture sensor readings
│ ├── 1 # Sensor 1 percentage
│ └── 2 # Sensor 2 percentage
├── schedule/ # Scheduling system
│ ├── status # Global scheduler status
│ ├── summary # Schedule summary (on demand)
│ ├── time/set # Manual time setting
│ ├── 1/ # Pump 1 schedules
│ │ ├── trigger # Manual trigger all schedules
│ │ ├── get # Get all schedules for pump
│ │ ├── executed # Execution notification
│ │ └── 0-3/ # Schedule slots
│ │ ├── config # Schedule configuration
│ │ ├── status # Schedule status (periodic)
│ │ └── current # Schedule details (on demand)
│ └── 2/ # Pump 2 (same structure)
├── system/ # System information
│ ├── time # Time response (on demand)
│ └── current_time # Current time (periodic)
├── commands/ # System commands
│ ├── emergency_stop # Stop all pumps immediately
│ ├── test_mode # Enable/disable test mode
│ ├── holiday_mode # Enable/disable all schedules
│ ├── get_time # Request current time
│ ├── get_schedules # Request all schedules
│ └── test_pump/ # Test pump operation
│ ├── 1 # Test pump 1
│ └── 2 # Test pump 2
├── settings/ # Runtime configuration
│ └── pump/
│ └── 1-2/
│ ├── max_runtime # Maximum runtime per cycle
│ ├── min_interval # Minimum time between runs
│ ├── min_speed # Minimum speed percentage
│ └── max_speed # Maximum speed percentage
└── alerts/ # System alerts
├── pump_error/1-2 # Pump errors
└── schedule_error/1-2 # Schedule execution errors
```
## Pump Control Topics
### Basic Pump Control
**Topic**: `plant_watering/pump/[1-2]/set`
**Direction**: Subscribe
**Payload**:
- `on` - Start pump at default speed (80%)
- `off` - Stop pump
- `pulse` - Run pump for 5 seconds
**Example**:
```bash
# Turn pump 1 on
mosquitto_pub -h <broker> -t "plant_watering/pump/1/set" -m "on"
# Turn pump 1 off
mosquitto_pub -h <broker> -t "plant_watering/pump/1/set" -m "off"
# Pulse pump 2
mosquitto_pub -h <broker> -t "plant_watering/pump/2/set" -m "pulse"
```
### Pump Speed Control
**Topic**: `plant_watering/pump/[1-2]/speed`
**Direction**: Subscribe
**Payload**: `0-100` (percentage)
**Note**: Only works when pump is running
**Example**:
```bash
# Set pump 1 to 50% speed
mosquitto_pub -h <broker> -t "plant_watering/pump/1/speed" -m "50"
```
### Pump State
**Topic**: `plant_watering/pump/[1-2]/state`
**Direction**: Publish
**Payload**: `on` or `off`
**Retained**: Yes
### Pump Statistics
**Topic**: `plant_watering/pump/[1-2]/stats`
**Direction**: Publish
**Payload**: JSON
```json
{
"total_runtime": 123456,
"run_count": 42,
"last_duration": 5000
}
```
### Pump Runtime (When Running)
**Topic**: `plant_watering/pump/[1-2]/runtime`
**Direction**: Publish
**Payload**: Current runtime in milliseconds
**Note**: Published every minute while pump is running
### Pump Cooldown Status
**Topic**: `plant_watering/pump/[1-2]/cooldown`
**Direction**: Publish
**Payload**: `true`
**Note**: Published when pump is in cooldown period
## Moisture Sensor Topics
### Moisture Readings
**Topic**: `plant_watering/moisture/[1-2]`
**Direction**: Publish
**Payload**: `0-100` (percentage)
**Frequency**: Every 10 seconds
**Note**: Currently simulated values
## Scheduling Topics
### Schedule Configuration
**Topic**: `plant_watering/schedule/[1-2]/[0-3]/config`
**Direction**: Subscribe
**Payload**: JSON configuration
**Schedule Types**:
1. **Interval Schedule**:
```json
{
"type": "interval",
"enabled": true,
"interval_minutes": 120,
"duration_ms": 15000,
"speed_percent": 70
}
```
2. **Time of Day Schedule**:
```json
{
"type": "time_of_day",
"enabled": true,
"hour": 6,
"minute": 30,
"duration_ms": 20000,
"speed_percent": 80
}
```
3. **Days and Time Schedule**:
```json
{
"type": "days_time",
"enabled": true,
"hour": 18,
"minute": 0,
"days_mask": 127,
"duration_ms": 25000,
"speed_percent": 75
}
```
4. **Disable Schedule**:
```json
{
"type": "disabled"
}
```
### Schedule Status
**Topic**: `plant_watering/schedule/[1-2]/[0-3]/status`
**Direction**: Publish
**Frequency**: Every minute (if enabled)
**Retained**: Yes
**Payload**: JSON with full schedule details including next run time
### Schedule Execution
**Topic**: `plant_watering/schedule/[1-2]/executed`
**Direction**: Publish
**Payload**: JSON
```json
{
"schedule_id": 0,
"duration_ms": 20000,
"speed": 80
}
```
### Global Schedule Status
**Topic**: `plant_watering/schedule/status`
**Direction**: Publish
**Frequency**: Every minute
**Retained**: Yes
**Payload**: JSON
```json
{
"holiday_mode": false,
"time_sync": true,
"active_schedules": 3,
"time": 1706436000
}
```
### Manual Schedule Trigger
**Topic**: `plant_watering/schedule/[1-2]/trigger`
**Direction**: Subscribe
**Payload**: Any value
**Action**: Triggers all enabled schedules for the specified pump
### Get Schedules
**Topic**: `plant_watering/schedule/[1-2]/get`
**Direction**: Subscribe
**Payload**: Any value
**Response**: Publishes all schedules for pump to `current` topics
### Manual Time Setting
**Topic**: `plant_watering/schedule/time/set`
**Direction**: Subscribe
**Payload**: Unix timestamp as string
**Example**: `"1706436000"`
## System Topics
### System Status
**Topic**: `plant_watering/status`
**Direction**: Publish
**Payload**: `online` or `offline`
**Retained**: Yes
**Note**: Uses MQTT Last Will and Testament
### Current Time (Periodic)
**Topic**: `plant_watering/system/current_time`
**Direction**: Publish
**Frequency**: Every minute
**Payload**: JSON
```json
{
"timestamp": 1706436000,
"datetime": "2024-01-28 14:32:00 MST",
"day_of_week": 0,
"hour": 14,
"minute": 32
}
```
### Time Response (On Demand)
**Topic**: `plant_watering/system/time`
**Direction**: Publish
**Trigger**: Send any message to `plant_watering/commands/get_time`
**Payload**: JSON
```json
{
"timestamp": 1706436000,
"datetime": "2024-01-28 14:32:45 MST",
"timezone": "MST7MDT,M3.2.0,M11.1.0",
"synced": true
}
```
## Command Topics
### Emergency Stop
**Topic**: `plant_watering/commands/emergency_stop`
**Direction**: Subscribe
**Payload**: Any value
**Action**: Immediately stops all pumps
### Test Mode
**Topic**: `plant_watering/commands/test_mode`
**Direction**: Subscribe
**Payload**: `on` or `off`
**Action**: Enables/disables test mode with shorter intervals
Test mode settings:
- Max runtime: 30 seconds
- Min interval: 5 seconds
### Holiday Mode
**Topic**: `plant_watering/commands/holiday_mode`
**Direction**: Subscribe
**Payload**: `on` or `off`
**Action**: Pauses all schedules without deleting them
### Get Time
**Topic**: `plant_watering/commands/get_time`
**Direction**: Subscribe
**Payload**: Any value
**Response**: Publishes to `plant_watering/system/time`
### Get All Schedules
**Topic**: `plant_watering/commands/get_schedules`
**Direction**: Subscribe
**Payload**: Any value
**Response**: Publishes all schedules and summary
### Test Pump
**Topic**: `plant_watering/commands/test_pump/[1-2]`
**Direction**: Subscribe
**Payload**: Duration in milliseconds (max 30000)
**Example**: `"15000"` for 15 seconds
## Settings Topics
### Pump Settings
**Base Topic**: `plant_watering/settings/pump/[1-2]/`
#### Maximum Runtime
**Topic**: `plant_watering/settings/pump/[1-2]/max_runtime`
**Payload**: Milliseconds (e.g., `"60000"` for 60 seconds)
#### Minimum Interval
**Topic**: `plant_watering/settings/pump/[1-2]/min_interval`
**Payload**: Milliseconds (e.g., `"300000"` for 5 minutes)
#### Speed Limits
**Topic**: `plant_watering/settings/pump/[1-2]/min_speed`
**Topic**: `plant_watering/settings/pump/[1-2]/max_speed`
**Payload**: Percentage 0-100
## Alert Topics
### Pump Errors
**Topic**: `plant_watering/alerts/pump_error/[1-2]`
**Direction**: Publish
**Payload**: Error description string
**Examples**:
- `"Maximum runtime exceeded"`
- `"Cooldown period not elapsed"`
### Schedule Errors
**Topic**: `plant_watering/alerts/schedule_error/[1-2]`
**Direction**: Publish
**Payload**: Error description
**Example**: `"Schedule 0 failed: ESP_ERR_INVALID_STATE"`
## Monitoring Examples
### Subscribe to All Topics
```bash
# Monitor everything
mosquitto_sub -h <broker> -t "plant_watering/#" -v
# Monitor pump activity only
mosquitto_sub -h <broker> -t "plant_watering/pump/#" -v
# Monitor schedules only
mosquitto_sub -h <broker> -t "plant_watering/schedule/#" -v
# Monitor alerts only
mosquitto_sub -h <broker> -t "plant_watering/alerts/#" -v
```
### Common Operations
#### Daily Morning Watering
```bash
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/0/config" -m '{
"type": "time_of_day",
"enabled": true,
"hour": 6,
"minute": 0,
"duration_ms": 20000,
"speed_percent": 80
}'
```
#### Check System Status
```bash
# Get current time
mosquitto_pub -h <broker> -t "plant_watering/commands/get_time" -m "1"
# Get all schedules
mosquitto_pub -h <broker> -t "plant_watering/commands/get_schedules" -m "1"
```
#### Manual Watering
```bash
# Run pump 1 for 15 seconds
mosquitto_pub -h <broker> -t "plant_watering/commands/test_pump/1" -m "15000"
```
#### Going on Vacation
```bash
# Enable holiday mode
mosquitto_pub -h <broker> -t "plant_watering/commands/holiday_mode" -m "on"
# When back, disable holiday mode
mosquitto_pub -h <broker> -t "plant_watering/commands/holiday_mode" -m "off"
```
## Notes
- All timestamps are Unix timestamps (seconds since epoch)
- All durations are in milliseconds
- Speed values are percentages (0-100)
- Day of week: 0=Sunday, 1=Monday, ..., 6=Saturday
- Days mask is a bitmask where bit 0=Sunday
- Retained messages persist across broker restarts
- JSON payloads use unquoted booleans (`true`/`false`)

422
SCHEDULER_README.md Normal file
View File

@ -0,0 +1,422 @@
# Scheduler Module
The scheduler module provides flexible time-based automation for the plant watering system, with NVS persistence and full MQTT control.
## Features
- **Multiple Schedule Types**:
- Interval-based (every X minutes)
- Time of day (daily at specific time)
- Day-specific (specific days at specific time)
- **Per-Pump Scheduling**: Up to 4 independent schedules per pump
- **NTP Time Synchronization**: Automatic internet time sync
- **Holiday Mode**: Pause all schedules without deleting them
- **MQTT Configuration**: Full remote control and monitoring
- **NVS Persistence**: Schedules survive power cycles
- **Manual Override**: Test schedules without waiting
- **No External Dependencies**: Built-in JSON handling
## Schedule Configuration
### Schedule Types
#### 1. Interval Schedule
Waters every X minutes from the last run time.
```json
{
"type": "interval",
"enabled": true,
"interval_minutes": 120,
"duration_ms": 15000,
"speed_percent": 70
}
```
#### 2. Time of Day Schedule
Waters daily at a specific time.
```json
{
"type": "time_of_day",
"enabled": true,
"hour": 6,
"minute": 30,
"duration_ms": 20000,
"speed_percent": 80
}
```
#### 3. Days and Time Schedule
Waters on specific days at a specific time.
```json
{
"type": "days_time",
"enabled": true,
"hour": 18,
"minute": 0,
"days_mask": 42,
"duration_ms": 25000,
"speed_percent": 75
}
```
### Days Mask Values
- Sunday: 1 (bit 0)
- Monday: 2 (bit 1)
- Tuesday: 4 (bit 2)
- Wednesday: 8 (bit 3)
- Thursday: 16 (bit 4)
- Friday: 32 (bit 5)
- Saturday: 64 (bit 6)
Common masks:
- Daily: 127 (all days)
- Weekdays: 62 (Mon-Fri)
- Weekends: 65 (Sat-Sun)
- Mon/Wed/Fri: 42
## MQTT Topics
### Schedule Configuration
Configure individual schedules for each pump.
**Topic**: `plant_watering/schedule/[pump_id]/[schedule_id]/config`
- pump_id: 1 or 2
- schedule_id: 0-3
**Example**: Configure pump 1, schedule 0 for daily 6:30 AM watering
```bash
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/0/config" -m '{
"type": "time_of_day",
"enabled": true,
"hour": 6,
"minute": 30,
"duration_ms": 20000,
"speed_percent": 80
}'
```
### View All Schedules
Get all configured schedules on demand.
**Topic**: `plant_watering/commands/get_schedules`
**Payload**: Any value (e.g., "1")
```bash
# Request all schedules
mosquitto_pub -h <broker> -t "plant_watering/commands/get_schedules" -m "1"
# Monitor the responses
mosquitto_sub -h <broker> -t "plant_watering/schedule/+/+/current" -t "plant_watering/schedule/summary" -v
```
**Response Topics**:
- `plant_watering/schedule/[pump_id]/[schedule_id]/current` - Each configured schedule
- `plant_watering/schedule/summary` - Summary of all schedules
Summary format:
```json
{
"total_schedules": 4,
"active_schedules": 3,
"holiday_mode": false,
"time_sync": true
}
```
### View Pump Schedules
Get schedules for a specific pump.
**Topic**: `plant_watering/schedule/[pump_id]/get`
**Payload**: Any value
```bash
# Get all schedules for pump 1
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/get" -m "1"
# Get all schedules for pump 2
mosquitto_pub -h <broker> -t "plant_watering/schedule/2/get" -m "1"
```
### Schedule Status
The system publishes schedule status after configuration and periodically.
**Topic**: `plant_watering/schedule/[pump_id]/[schedule_id]/status`
**Response**:
```json
{
"pump_id": 1,
"schedule_id": 0,
"type": "time_of_day",
"enabled": true,
"hour": 6,
"minute": 30,
"duration_ms": 20000,
"speed_percent": 80,
"next_run": 1706522400,
"next_run_str": "2024-01-29 06:30:00",
"last_run": 1706436000
}
```
### Global Scheduler Status
Published every minute when connected.
**Topic**: `plant_watering/schedule/status`
**Format**:
```json
{
"holiday_mode": false,
"time_sync": true,
"active_schedules": 3,
"time": 1706436000
}
```
### Current System Time
Check or monitor the device's current time.
**Get Time (On Demand)**
- **Topic**: `plant_watering/commands/get_time`
- **Payload**: Any value (e.g., "1")
- **Response Topic**: `plant_watering/system/time`
```bash
# Request current time
mosquitto_pub -h <broker> -t "plant_watering/commands/get_time" -m "1"
```
Response:
```json
{
"timestamp": 1706436000,
"datetime": "2024-01-28 14:32:45 MST",
"timezone": "MST7MDT,M3.2.0,M11.1.0",
"synced": true
}
```
**Periodic Time (Every Minute)**
- **Topic**: `plant_watering/system/current_time`
Format:
```json
{
"timestamp": 1706436000,
"datetime": "2024-01-28 14:32:00 MST",
"day_of_week": 0,
"hour": 14,
"minute": 32
}
```
### Holiday Mode
Pause all schedules without deleting them.
**Topic**: `plant_watering/commands/holiday_mode`
**Payload**: `on` or `off`
```bash
# Enable holiday mode
mosquitto_pub -h <broker> -t "plant_watering/commands/holiday_mode" -m "on"
# Disable holiday mode
mosquitto_pub -h <broker> -t "plant_watering/commands/holiday_mode" -m "off"
```
### Manual Time Setting
If NTP is unavailable, set time manually.
**Topic**: `plant_watering/schedule/time/set`
**Payload**: Unix timestamp (as string)
```bash
# Set current time
mosquitto_pub -h <broker> -t "plant_watering/schedule/time/set" -m "$(date +%s)"
```
### Manual Schedule Trigger
Test schedules without waiting.
**Topic**: `plant_watering/schedule/[pump_id]/trigger`
```bash
# Trigger all enabled schedules for pump 1
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/trigger" -m "1"
```
### Schedule Execution Notification
Published when a schedule executes.
**Topic**: `plant_watering/schedule/[pump_id]/executed`
**Format**:
```json
{
"schedule_id": 0,
"duration_ms": 20000,
"speed": 80
}
```
## Example Configurations
### Example 1: Morning and Evening Watering
```bash
# Morning watering at 6:30 AM
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/0/config" -m '{
"type": "time_of_day",
"enabled": true,
"hour": 6,
"minute": 30,
"duration_ms": 15000,
"speed_percent": 70
}'
# Evening watering at 6:30 PM
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/1/config" -m '{
"type": "time_of_day",
"enabled": true,
"hour": 18,
"minute": 30,
"duration_ms": 15000,
"speed_percent": 70
}'
```
### Example 2: Every 2 Hours During Day
```bash
# Interval watering every 2 hours
mosquitto_pub -h <broker> -t "plant_watering/schedule/2/0/config" -m '{
"type": "interval",
"enabled": true,
"interval_minutes": 120,
"duration_ms": 10000,
"speed_percent": 60
}'
```
### Example 3: Weekday Morning Watering
```bash
# Water Monday-Friday at 7:00 AM
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/2/config" -m '{
"type": "days_time",
"enabled": true,
"hour": 7,
"minute": 0,
"days_mask": 62,
"duration_ms": 20000,
"speed_percent": 80
}'
```
### Example 4: Different Weekend Schedule
```bash
# Weekend watering at 9:00 AM with longer duration
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/3/config" -m '{
"type": "days_time",
"enabled": true,
"hour": 9,
"minute": 0,
"days_mask": 65,
"duration_ms": 30000,
"speed_percent": 75
}'
```
## Disable/Enable Schedules
To disable a schedule without deleting it:
```bash
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/0/config" -m '{
"type": "time_of_day",
"enabled": false,
"hour": 6,
"minute": 30,
"duration_ms": 20000,
"speed_percent": 80
}'
```
To completely remove a schedule:
```bash
mosquitto_pub -h <broker> -t "plant_watering/schedule/1/0/config" -m '{
"type": "disabled"
}'
```
## Time Zone Configuration
The scheduler uses Mountain Time (MST/MDT) by default. To change:
1. Edit `scheduler.c` in the `scheduler_init()` function:
```c
// Set timezone (adjust as needed)
setenv("TZ", "PST8PDT,M3.2.0,M11.1.0", 1); // Pacific Time
setenv("TZ", "EST5EDT,M3.2.0,M11.1.0", 1); // Eastern Time
setenv("TZ", "CST6CDT,M3.2.0,M11.1.0", 1); // Central Time
setenv("TZ", "GMT0BST,M3.5.0,M10.5.0", 1); // UK Time
```
## Serial Monitor Output
The system status is printed to serial every 30 seconds, including:
```
I (xxxxx) MAIN: Scheduler: 2 active, Holiday: OFF, DateTime: 2024-01-28 14:32:45
```
## Troubleshooting
### Time Not Synchronized
- Check internet connection
- Verify NTP servers are accessible
- Manual set time as workaround
- Check serial output for sync status
### Schedule Not Executing
1. Check if time is synchronized
2. Verify schedule is enabled
3. Check holiday mode is off
4. Verify schedule configuration is valid
5. Check pump isn't in cooldown period
6. Monitor execution notifications on MQTT
### Schedule Executes at Wrong Time
- Verify timezone setting
- Check system time is correct
- Remember schedules won't run twice within 60 seconds
- Use `get_time` command to verify device time
## Integration with Automation
The scheduler can work alongside moisture-based automation:
- Schedules provide baseline watering
- Moisture sensors can trigger additional watering
- Both respect motor safety limits (max runtime, cooldown)
## Best Practices
1. **Test Schedules**: Use manual trigger to test before relying on schedule
2. **Start Simple**: Begin with one schedule and add more as needed
3. **Monitor Execution**: Watch MQTT topics to confirm schedules work
4. **Use Holiday Mode**: Don't delete schedules when going away
5. **Stagger Schedules**: If using multiple pumps, offset times to reduce load
6. **Monitor Time Sync**: Ensure device maintains correct time
## Implementation Notes
- **No External Dependencies**: The scheduler uses built-in JSON parsing instead of cJSON library
- **Time Check Interval**: Schedules are checked every 30 seconds
- **Execution Window**: Schedules execute within 60 seconds of target time
- **NTP Servers**: Uses pool.ntp.org, time.nist.gov, and time.google.com
- **Persistence**: Schedule configurations saved to NVS, runtime info is not persisted
## Limitations
- Maximum 4 schedules per pump (8 total)
- Minimum resolution is 1 minute
- Schedules check every 30 seconds (may be up to 30s late)
- All times are in configured timezone
- Requires accurate system time (NTP or manual)
- Simple JSON parser has basic error handling

View File

@ -1,2 +1,8 @@
docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 mosquitto_pub -h mosquitto -u home-server -P '123QWeaSDZXC!@#' -t "home/plants/pump1/command" -m "OFF" -r docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 mosquitto_pub -h mosquitto -u home-server -P '123QWeaSDZXC!@#' -t "home/plants/pump1/command" -m "OFF" -r
docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 mosquitto_sub -h mosquitto -u monitor -P ThisIsNotATest123monitor -t "home/plants/#" -v docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 mosquitto_sub -h mosquitto -u monitor -P ThisIsNotATest123monitor -t "home/plants/#" -v
docker run --user $(id -u):$(id -g) --rm -v $PWD:/project -w /project -it espressif/idf:latest idf.py build
docker run --privileged --rm -v $PWD:/project -w /project --device=/dev/ttyACM0 -it espressif/idf:latest idf.py monitor -p /dev/ttyACM0
(If ota does not work)
docker run --privileged --rm -v $PWD:/project -w /project --device=/dev/ttyACM0 -it espressif/idf:latest idf.py flash -p /dev/ttyACM0

View File

@ -6,6 +6,7 @@ idf_component_register(
"plant_mqtt.c" "plant_mqtt.c"
"led_strip.c" "led_strip.c"
"motor_control.c" "motor_control.c"
"scheduler.c"
INCLUDE_DIRS INCLUDE_DIRS
"." "."
REQUIRES REQUIRES

View File

@ -10,6 +10,7 @@
#include "ota_server.h" #include "ota_server.h"
#include "plant_mqtt.h" #include "plant_mqtt.h"
#include "motor_control.h" #include "motor_control.h"
#include "scheduler.h"
#include "sdkconfig.h" #include "sdkconfig.h"
// Uncomment this line to enable motor test mode with shorter intervals // Uncomment this line to enable motor test mode with shorter intervals
@ -18,12 +19,15 @@
static const char *TAG = "MAIN"; static const char *TAG = "MAIN";
// Application version // Application version
#define APP_VERSION "2.1.0-motor" #define APP_VERSION "2.2.0-scheduler"
// Test data // Test data
static int test_moisture_1 = 45; static int test_moisture_1 = 45;
static int test_moisture_2 = 62; static int test_moisture_2 = 62;
// Function prototypes
static void print_chip_info(void);
// Motor Control Callbacks // Motor Control Callbacks
static void motor_state_change_callback(motor_id_t id, motor_state_t state) static void motor_state_change_callback(motor_id_t id, motor_state_t state)
{ {
@ -63,6 +67,40 @@ static void motor_error_callback(motor_id_t id, const char* error)
} }
} }
// Scheduler callback
static void scheduler_trigger_callback(uint8_t pump_id, uint8_t schedule_id,
uint32_t duration_ms, uint8_t speed_percent)
{
ESP_LOGI(TAG, "Schedule %d triggered for pump %d: %lu ms at %d%%",
schedule_id, pump_id, duration_ms, speed_percent);
// Start the pump with the scheduled parameters
esp_err_t ret = motor_start_timed(pump_id, speed_percent, duration_ms);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start pump %d for schedule %d: %s",
pump_id, schedule_id, esp_err_to_name(ret));
// Publish error to MQTT
if (mqtt_client_is_connected()) {
char topic[64];
char msg[128];
snprintf(topic, sizeof(topic), "plant_watering/alerts/schedule_error/%d", pump_id);
snprintf(msg, sizeof(msg), "Schedule %d failed: %s", schedule_id, esp_err_to_name(ret));
mqtt_client_publish(topic, msg, MQTT_QOS_1, MQTT_NO_RETAIN);
}
} else {
// Publish schedule execution to MQTT
if (mqtt_client_is_connected()) {
char topic[64];
char msg[128];
snprintf(topic, sizeof(topic), "plant_watering/schedule/%d/executed", pump_id);
snprintf(msg, sizeof(msg), "{\"schedule_id\":%d,\"duration_ms\":%lu,\"speed\":%d}",
schedule_id, duration_ms, speed_percent);
mqtt_client_publish(topic, msg, MQTT_QOS_0, MQTT_NO_RETAIN);
}
}
}
// MQTT Callbacks // MQTT Callbacks
static void mqtt_connected_callback(void) static void mqtt_connected_callback(void)
{ {
@ -80,6 +118,13 @@ static void mqtt_connected_callback(void)
"plant_watering/commands/test_pump/+", "plant_watering/commands/test_pump/+",
"plant_watering/commands/emergency_stop", "plant_watering/commands/emergency_stop",
"plant_watering/commands/test_mode", "plant_watering/commands/test_mode",
"plant_watering/commands/holiday_mode",
"plant_watering/commands/get_time",
"plant_watering/commands/get_schedules",
"plant_watering/schedule/+/+/config",
"plant_watering/schedule/+/trigger",
"plant_watering/schedule/+/get",
"plant_watering/schedule/time/set",
"plant_watering/settings/+/+", "plant_watering/settings/+/+",
NULL NULL
}; };
@ -196,13 +241,148 @@ static void mqtt_data_callback(const char* topic, const char* data, int data_len
motor_set_max_runtime(MOTOR_PUMP_1, CONFIG_WATERING_MAX_DURATION_MS); motor_set_max_runtime(MOTOR_PUMP_1, CONFIG_WATERING_MAX_DURATION_MS);
motor_set_max_runtime(MOTOR_PUMP_2, CONFIG_WATERING_MAX_DURATION_MS); motor_set_max_runtime(MOTOR_PUMP_2, CONFIG_WATERING_MAX_DURATION_MS);
} }
} else if (strncmp(topic, "plant_watering/settings/pump/", 29) == 0) { } else if (strcmp(topic, "plant_watering/commands/holiday_mode") == 0) {
// Parse settings commands like: if (strncmp(data, "on", data_len) == 0) {
// plant_watering/settings/pump/1/max_runtime scheduler_set_holiday_mode(true);
// plant_watering/settings/pump/1/min_interval ESP_LOGI(TAG, "Holiday mode enabled - all schedules paused");
// plant_watering/settings/pump/1/min_speed } else if (strncmp(data, "off", data_len) == 0) {
// plant_watering/settings/pump/1/max_speed scheduler_set_holiday_mode(false);
ESP_LOGI(TAG, "Holiday mode disabled - schedules resumed");
}
} else if (strcmp(topic, "plant_watering/commands/get_time") == 0) {
// Publish current time information
if (scheduler_is_time_synchronized()) {
time_t now = scheduler_get_current_time();
struct tm timeinfo;
localtime_r(&now, &timeinfo);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &timeinfo);
char response[256];
snprintf(response, sizeof(response),
"{\"timestamp\":%lld,\"datetime\":\"%s\",\"timezone\":\"%s\",\"synced\":true}",
(long long)now, time_str, getenv("TZ") ? getenv("TZ") : "UTC");
mqtt_client_publish("plant_watering/system/time", response, MQTT_QOS_0, MQTT_NO_RETAIN);
ESP_LOGI(TAG, "Time: %s", time_str);
} else {
mqtt_client_publish("plant_watering/system/time",
"{\"synced\":false,\"message\":\"Time not synchronized\"}",
MQTT_QOS_0, MQTT_NO_RETAIN);
ESP_LOGW(TAG, "Time not synchronized");
}
} else if (strcmp(topic, "plant_watering/commands/get_schedules") == 0) {
// Publish all configured schedules
ESP_LOGI(TAG, "Publishing all schedules");
int active_count = 0;
// Publish each configured schedule
for (int pump = 1; pump <= 2; pump++) {
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
schedule_config_t config;
if (scheduler_get_schedule(pump, sched, &config) == ESP_OK) {
// Only publish if schedule is configured (not disabled)
if (config.type != SCHEDULE_TYPE_DISABLED) {
char topic_buf[64];
char json[512];
snprintf(topic_buf, sizeof(topic_buf),
"plant_watering/schedule/%d/%d/current", pump, sched);
if (scheduler_schedule_to_json(pump, sched, json, sizeof(json)) == ESP_OK) {
mqtt_client_publish(topic_buf, json, MQTT_QOS_0, MQTT_NO_RETAIN);
if (config.enabled) {
active_count++;
}
}
}
}
}
}
// Publish summary
char summary[256];
snprintf(summary, sizeof(summary),
"{\"total_schedules\":%d,\"active_schedules\":%d,\"holiday_mode\":%s,\"time_sync\":%s}",
active_count,
active_count,
scheduler_get_holiday_mode() ? "true" : "false",
scheduler_is_time_synchronized() ? "true" : "false");
mqtt_client_publish("plant_watering/schedule/summary", summary, MQTT_QOS_0, MQTT_NO_RETAIN);
ESP_LOGI(TAG, "Published %d schedules", active_count);
} else if (strncmp(topic, "plant_watering/schedule/", 24) == 0) {
// Parse schedule commands
if (strcmp(topic, "plant_watering/schedule/time/set") == 0) {
// Set system time manually (useful if no NTP)
time_t timestamp = atoll(data); // Use atoll for long long
if (timestamp > 0) {
scheduler_set_time(timestamp);
ESP_LOGI(TAG, "System time set to %lld", (long long)timestamp);
}
} else {
int pump_id = 0;
int schedule_id = 0;
char action[16] = {0};
int parsed = sscanf(topic + 24, "%d/%d/%15s", &pump_id, &schedule_id, action);
if (parsed == 2) {
// Check if it's a trigger command
if (sscanf(topic + 24, "%d/trigger", &pump_id) == 1) {
if (pump_id >= 1 && pump_id <= 2) {
// Trigger all enabled schedules for this pump
ESP_LOGI(TAG, "Manual trigger for pump %d schedules", pump_id);
for (int i = 0; i < SCHEDULER_MAX_SCHEDULES_PER_PUMP; i++) {
scheduler_trigger_schedule(pump_id, i);
}
}
}
// Check if it's a get command for specific pump
else if (sscanf(topic + 24, "%d/get", &pump_id) == 1) {
if (pump_id >= 1 && pump_id <= 2) {
ESP_LOGI(TAG, "Getting schedules for pump %d", pump_id);
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
schedule_config_t config;
if (scheduler_get_schedule(pump_id, sched, &config) == ESP_OK &&
config.type != SCHEDULE_TYPE_DISABLED) {
char topic_buf[64];
char json[512];
snprintf(topic_buf, sizeof(topic_buf),
"plant_watering/schedule/%d/%d/current", pump_id, sched);
if (scheduler_schedule_to_json(pump_id, sched, json, sizeof(json)) == ESP_OK) {
mqtt_client_publish(topic_buf, json, MQTT_QOS_0, MQTT_NO_RETAIN);
}
}
}
}
}
} else if (parsed == 3 && strcmp(action, "config") == 0) {
// Configure schedule
if (pump_id >= 1 && pump_id <= 2 &&
schedule_id >= 0 && schedule_id < SCHEDULER_MAX_SCHEDULES_PER_PUMP) {
esp_err_t ret = scheduler_json_to_schedule(data, pump_id, schedule_id);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Updated schedule %d for pump %d", schedule_id, pump_id);
// Publish confirmation
char response_topic[64];
char response[512];
snprintf(response_topic, sizeof(response_topic),
"plant_watering/schedule/%d/%d/status", pump_id, schedule_id);
scheduler_schedule_to_json(pump_id, schedule_id, response, sizeof(response));
mqtt_client_publish(response_topic, response, MQTT_QOS_0, MQTT_RETAIN);
} else {
ESP_LOGE(TAG, "Failed to update schedule: %s", esp_err_to_name(ret));
}
}
}
}
} else if (strncmp(topic, "plant_watering/settings/pump/", 29) == 0) {
// Parse settings commands
int pump_id = 0; int pump_id = 0;
char setting[32] = {0}; char setting[32] = {0};
@ -218,10 +398,7 @@ static void mqtt_data_callback(const char* topic, const char* data, int data_len
motor_set_min_interval(pump_id, value); motor_set_min_interval(pump_id, value);
ESP_LOGI(TAG, "Set pump %d min interval to %d ms", pump_id, value); ESP_LOGI(TAG, "Set pump %d min interval to %d ms", pump_id, value);
} else if (strcmp(setting, "min_speed") == 0 && value >= 0 && value <= 100) { } else if (strcmp(setting, "min_speed") == 0 && value >= 0 && value <= 100) {
// Get current max speed to validate motor_set_speed_limits(pump_id, value, 100);
motor_stats_t stats;
motor_get_stats(pump_id, &stats);
motor_set_speed_limits(pump_id, value, 100); // Assuming max stays at 100
ESP_LOGI(TAG, "Set pump %d min speed to %d%%", pump_id, value); ESP_LOGI(TAG, "Set pump %d min speed to %d%%", pump_id, value);
} else if (strcmp(setting, "max_speed") == 0 && value > 0 && value <= 100) { } else if (strcmp(setting, "max_speed") == 0 && value > 0 && value <= 100) {
motor_set_speed_limits(pump_id, MOTOR_MIN_SPEED, value); motor_set_speed_limits(pump_id, MOTOR_MIN_SPEED, value);
@ -321,34 +498,62 @@ static void sensor_simulation_task(void *pvParameters)
} }
} }
// Task to demonstrate automated watering based on moisture // Task to publish schedule status periodically
static void automation_demo_task(void *pvParameters) static void schedule_status_task(void *pvParameters)
{ {
bool auto_mode = false; // Start with manual mode
while (1) { while (1) {
if (auto_mode && mqtt_client_is_connected()) { vTaskDelay(pdMS_TO_TICKS(60000)); // Every minute
// Simple threshold-based automation demo
if (test_moisture_1 < CONFIG_MOISTURE_THRESHOLD_LOW) { if (mqtt_client_is_connected() && scheduler_is_time_synchronized()) {
if (!motor_is_running(MOTOR_PUMP_1) && !motor_is_cooldown(MOTOR_PUMP_1)) { // Publish scheduler status
ESP_LOGI(TAG, "Auto: Moisture 1 low (%d%%), starting pump 1", test_moisture_1); scheduler_status_t status;
motor_start_timed(MOTOR_PUMP_1, MOTOR_DEFAULT_SPEED, 10000); // 10 second watering if (scheduler_get_status(&status) == ESP_OK) {
} char status_json[256];
snprintf(status_json, sizeof(status_json),
"{\"holiday_mode\":%s,\"time_sync\":%s,\"active_schedules\":%lu,\"time\":%lld}",
status.holiday_mode ? "true" : "false",
status.time_synchronized ? "true" : "false",
status.active_schedules,
(long long)scheduler_get_current_time());
mqtt_client_publish("plant_watering/schedule/status", status_json, MQTT_QOS_0, MQTT_RETAIN);
} }
if (test_moisture_2 < CONFIG_MOISTURE_THRESHOLD_LOW) { // Publish human-readable time periodically
if (!motor_is_running(MOTOR_PUMP_2) && !motor_is_cooldown(MOTOR_PUMP_2)) { time_t now = scheduler_get_current_time();
ESP_LOGI(TAG, "Auto: Moisture 2 low (%d%%), starting pump 2", test_moisture_2); struct tm timeinfo;
motor_start_timed(MOTOR_PUMP_2, MOTOR_DEFAULT_SPEED, 10000); // 10 second watering localtime_r(&now, &timeinfo);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &timeinfo);
char time_json[256];
snprintf(time_json, sizeof(time_json),
"{\"timestamp\":%lld,\"datetime\":\"%s\",\"day_of_week\":%d,\"hour\":%d,\"minute\":%d}",
(long long)now, time_str, timeinfo.tm_wday, timeinfo.tm_hour, timeinfo.tm_min);
mqtt_client_publish("plant_watering/system/current_time", time_json, MQTT_QOS_0, MQTT_NO_RETAIN);
// Publish all active schedules
for (int pump = 1; pump <= 2; pump++) {
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
schedule_config_t config;
if (scheduler_get_schedule(pump, sched, &config) == ESP_OK &&
config.enabled && config.type != SCHEDULE_TYPE_DISABLED) {
char topic[64];
char json[512];
snprintf(topic, sizeof(topic), "plant_watering/schedule/%d/%d/status", pump, sched);
if (scheduler_schedule_to_json(pump, sched, json, sizeof(json)) == ESP_OK) {
mqtt_client_publish(topic, json, MQTT_QOS_0, MQTT_RETAIN);
}
}
}
}
} }
} }
} }
vTaskDelay(30000 / portTICK_PERIOD_MS); // Check every 30 seconds static void print_chip_info(void)
}
}
void print_chip_info(void)
{ {
esp_chip_info_t chip_info; esp_chip_info_t chip_info;
@ -372,6 +577,7 @@ void app_main(void)
// Print configuration // Print configuration
ESP_LOGI(TAG, "Configuration:"); ESP_LOGI(TAG, "Configuration:");
ESP_LOGI(TAG, " MQTT Broker: %s", CONFIG_MQTT_BROKER_URL);
ESP_LOGI(TAG, " Moisture threshold low: %d%%", CONFIG_MOISTURE_THRESHOLD_LOW); ESP_LOGI(TAG, " Moisture threshold low: %d%%", CONFIG_MOISTURE_THRESHOLD_LOW);
ESP_LOGI(TAG, " Moisture threshold high: %d%%", CONFIG_MOISTURE_THRESHOLD_HIGH); ESP_LOGI(TAG, " Moisture threshold high: %d%%", CONFIG_MOISTURE_THRESHOLD_HIGH);
ESP_LOGI(TAG, " Max watering duration: %d ms", CONFIG_WATERING_MAX_DURATION_MS); ESP_LOGI(TAG, " Max watering duration: %d ms", CONFIG_WATERING_MAX_DURATION_MS);
@ -413,6 +619,10 @@ void app_main(void)
motor_set_min_interval(MOTOR_PUMP_2, CONFIG_WATERING_MIN_INTERVAL_MS); motor_set_min_interval(MOTOR_PUMP_2, CONFIG_WATERING_MIN_INTERVAL_MS);
#endif #endif
// Initialize Scheduler
ESP_ERROR_CHECK(scheduler_init());
scheduler_register_trigger_callback(scheduler_trigger_callback);
// Start WiFi connection // Start WiFi connection
esp_err_t ret = wifi_manager_start(); esp_err_t ret = wifi_manager_start();
if (ret != ESP_OK) { if (ret != ESP_OK) {
@ -422,14 +632,15 @@ void app_main(void)
// Create sensor simulation task // Create sensor simulation task
xTaskCreate(sensor_simulation_task, "sensor_sim", 4096, NULL, 5, NULL); xTaskCreate(sensor_simulation_task, "sensor_sim", 4096, NULL, 5, NULL);
// Create automation demo task (disabled by default) // Create schedule status task
xTaskCreate(automation_demo_task, "automation", 4096, NULL, 4, NULL); xTaskCreate(schedule_status_task, "schedule_status", 4096, NULL, 4, NULL);
// Main loop - monitor system status // Main loop - monitor system status
while (1) { while (1) {
ESP_LOGI(TAG, "System Status - WiFi: %s, MQTT: %s, Free heap: %d bytes", ESP_LOGI(TAG, "System Status - WiFi: %s, MQTT: %s, Time: %s, Free heap: %d bytes",
wifi_manager_is_connected() ? "Connected" : "Disconnected", wifi_manager_is_connected() ? "Connected" : "Disconnected",
mqtt_client_is_connected() ? "Connected" : "Disconnected", mqtt_client_is_connected() ? "Connected" : "Disconnected",
scheduler_is_time_synchronized() ? "Synced" : "Not synced",
esp_get_free_heap_size()); esp_get_free_heap_size());
// Print pump states and runtime // Print pump states and runtime
@ -448,6 +659,24 @@ void app_main(void)
ESP_LOGI(TAG, "Pump %d: %s, Total runtime: %lu s, Runs: %lu", ESP_LOGI(TAG, "Pump %d: %s, Total runtime: %lu s, Runs: %lu",
i, state_str, stats.total_runtime_ms / 1000, stats.run_count); i, state_str, stats.total_runtime_ms / 1000, stats.run_count);
} }
// Print scheduler status
if (scheduler_is_time_synchronized()) {
scheduler_status_t sched_status;
if (scheduler_get_status(&sched_status) == ESP_OK) {
time_t now = scheduler_get_current_time();
struct tm timeinfo;
localtime_r(&now, &timeinfo);
char datetime_str[32];
strftime(datetime_str, sizeof(datetime_str), "%Y-%m-%d %H:%M:%S", &timeinfo);
ESP_LOGI(TAG, "Scheduler: %d active, Holiday: %s, DateTime: %s",
sched_status.active_schedules,
sched_status.holiday_mode ? "ON" : "OFF",
datetime_str);
}
}
} }
vTaskDelay(30000 / portTICK_PERIOD_MS); // Every 30 seconds vTaskDelay(30000 / portTICK_PERIOD_MS); // Every 30 seconds

871
main/scheduler.c Normal file
View File

@ -0,0 +1,871 @@
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_sntp.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "scheduler.h"
static const char *TAG = "SCHEDULER";
// NVS namespace
#define SCHEDULER_NVS_NAMESPACE "scheduler"
// Scheduler state
typedef struct {
bool initialized;
bool time_synchronized;
time_t last_sync_time;
bool holiday_mode;
// Schedules storage
schedule_config_t schedules[SCHEDULER_MAX_PUMPS][SCHEDULER_MAX_SCHEDULES_PER_PUMP];
// Task handle
TaskHandle_t scheduler_task;
SemaphoreHandle_t mutex;
// Callbacks
schedule_trigger_callback_t trigger_callback;
schedule_status_callback_t status_callback;
} scheduler_state_t;
static scheduler_state_t s_scheduler = {0};
// Forward declarations
static void scheduler_task(void *pvParameters);
static esp_err_t save_schedule_to_nvs(uint8_t pump_id, uint8_t schedule_id);
static esp_err_t load_schedule_from_nvs(uint8_t pump_id, uint8_t schedule_id);
static esp_err_t save_global_settings(void);
static esp_err_t load_global_settings(void);
static void check_and_execute_schedules(void);
static bool should_run_now(const schedule_config_t *config, time_t current_time);
// NTP sync callback
static void time_sync_notification_cb(struct timeval *tv)
{
ESP_LOGI(TAG, "Time synchronized via NTP");
s_scheduler.time_synchronized = true;
s_scheduler.last_sync_time = tv->tv_sec;
}
esp_err_t scheduler_init(void)
{
if (s_scheduler.initialized) {
return ESP_OK;
}
ESP_LOGI(TAG, "Initializing scheduler");
// Create mutex
s_scheduler.mutex = xSemaphoreCreateMutex();
if (s_scheduler.mutex == NULL) {
ESP_LOGE(TAG, "Failed to create mutex");
return ESP_ERR_NO_MEM;
}
// Initialize schedules array
memset(s_scheduler.schedules, 0, sizeof(s_scheduler.schedules));
// Load schedules from NVS
for (int pump = 0; pump < SCHEDULER_MAX_PUMPS; pump++) {
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
load_schedule_from_nvs(pump + 1, sched);
}
}
// Load global settings
load_global_settings();
// Initialize SNTP for time synchronization
ESP_LOGI(TAG, "Initializing SNTP");
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
esp_sntp_setservername(0, "pool.ntp.org");
esp_sntp_setservername(1, "time.nist.gov");
esp_sntp_setservername(2, "time.google.com");
sntp_set_time_sync_notification_cb(time_sync_notification_cb);
esp_sntp_init();
// Set timezone (adjust as needed)
setenv("TZ", "MST7MDT,M3.2.0,M11.1.0", 1); // Mountain Time (Denver)
tzset();
// Create scheduler task
if (xTaskCreate(scheduler_task, "scheduler", 4096, NULL, 5, &s_scheduler.scheduler_task) != pdPASS) {
ESP_LOGE(TAG, "Failed to create scheduler task");
vSemaphoreDelete(s_scheduler.mutex);
return ESP_ERR_NO_MEM;
}
s_scheduler.initialized = true;
ESP_LOGI(TAG, "Scheduler initialized successfully");
return ESP_OK;
}
esp_err_t scheduler_deinit(void)
{
if (!s_scheduler.initialized) {
return ESP_OK;
}
// Stop SNTP
esp_sntp_stop();
// Delete task
if (s_scheduler.scheduler_task) {
vTaskDelete(s_scheduler.scheduler_task);
}
// Delete mutex
if (s_scheduler.mutex) {
vSemaphoreDelete(s_scheduler.mutex);
}
s_scheduler.initialized = false;
return ESP_OK;
}
esp_err_t scheduler_add_schedule(uint8_t pump_id, uint8_t schedule_id,
const schedule_config_t *config)
{
if (!s_scheduler.initialized || !config) {
return ESP_ERR_INVALID_STATE;
}
if (pump_id < 1 || pump_id > SCHEDULER_MAX_PUMPS ||
schedule_id >= SCHEDULER_MAX_SCHEDULES_PER_PUMP) {
return ESP_ERR_INVALID_ARG;
}
if (config->type >= SCHEDULE_TYPE_MAX) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_scheduler.mutex, portMAX_DELAY);
// Copy configuration
memcpy(&s_scheduler.schedules[pump_id - 1][schedule_id], config, sizeof(schedule_config_t));
// Calculate next run time
if (config->enabled && s_scheduler.time_synchronized) {
time_t now = scheduler_get_current_time();
s_scheduler.schedules[pump_id - 1][schedule_id].next_run =
scheduler_calculate_next_run(config, now);
}
// Save to NVS
esp_err_t ret = save_schedule_to_nvs(pump_id, schedule_id);
xSemaphoreGive(s_scheduler.mutex);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Added schedule %d for pump %d", schedule_id, pump_id);
}
return ret;
}
esp_err_t scheduler_get_schedule(uint8_t pump_id, uint8_t schedule_id,
schedule_config_t *config)
{
if (!s_scheduler.initialized || !config) {
return ESP_ERR_INVALID_STATE;
}
if (pump_id < 1 || pump_id > SCHEDULER_MAX_PUMPS ||
schedule_id >= SCHEDULER_MAX_SCHEDULES_PER_PUMP) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_scheduler.mutex, portMAX_DELAY);
memcpy(config, &s_scheduler.schedules[pump_id - 1][schedule_id], sizeof(schedule_config_t));
xSemaphoreGive(s_scheduler.mutex);
return ESP_OK;
}
esp_err_t scheduler_remove_schedule(uint8_t pump_id, uint8_t schedule_id)
{
if (!s_scheduler.initialized) {
return ESP_ERR_INVALID_STATE;
}
if (pump_id < 1 || pump_id > SCHEDULER_MAX_PUMPS ||
schedule_id >= SCHEDULER_MAX_SCHEDULES_PER_PUMP) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_scheduler.mutex, portMAX_DELAY);
// Clear schedule
memset(&s_scheduler.schedules[pump_id - 1][schedule_id], 0, sizeof(schedule_config_t));
// Remove from NVS
nvs_handle_t nvs_handle;
esp_err_t ret = nvs_open(SCHEDULER_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
if (ret == ESP_OK) {
char key[32];
snprintf(key, sizeof(key), "sched_%d_%d", pump_id, schedule_id);
nvs_erase_key(nvs_handle, key);
nvs_commit(nvs_handle);
nvs_close(nvs_handle);
}
xSemaphoreGive(s_scheduler.mutex);
ESP_LOGI(TAG, "Removed schedule %d for pump %d", schedule_id, pump_id);
return ret;
}
esp_err_t scheduler_enable_schedule(uint8_t pump_id, uint8_t schedule_id, bool enable)
{
if (!s_scheduler.initialized) {
return ESP_ERR_INVALID_STATE;
}
if (pump_id < 1 || pump_id > SCHEDULER_MAX_PUMPS ||
schedule_id >= SCHEDULER_MAX_SCHEDULES_PER_PUMP) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_scheduler.mutex, portMAX_DELAY);
s_scheduler.schedules[pump_id - 1][schedule_id].enabled = enable;
// Recalculate next run time if enabling
if (enable && s_scheduler.time_synchronized) {
time_t now = scheduler_get_current_time();
s_scheduler.schedules[pump_id - 1][schedule_id].next_run =
scheduler_calculate_next_run(&s_scheduler.schedules[pump_id - 1][schedule_id], now);
}
esp_err_t ret = save_schedule_to_nvs(pump_id, schedule_id);
xSemaphoreGive(s_scheduler.mutex);
ESP_LOGI(TAG, "%s schedule %d for pump %d", enable ? "Enabled" : "Disabled",
schedule_id, pump_id);
return ret;
}
esp_err_t scheduler_set_time(time_t current_time)
{
struct timeval tv = {
.tv_sec = current_time,
.tv_usec = 0
};
settimeofday(&tv, NULL);
s_scheduler.time_synchronized = true;
s_scheduler.last_sync_time = current_time;
// Recalculate all next run times
xSemaphoreTake(s_scheduler.mutex, portMAX_DELAY);
for (int pump = 0; pump < SCHEDULER_MAX_PUMPS; pump++) {
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
if (s_scheduler.schedules[pump][sched].enabled) {
s_scheduler.schedules[pump][sched].next_run =
scheduler_calculate_next_run(&s_scheduler.schedules[pump][sched], current_time);
}
}
}
xSemaphoreGive(s_scheduler.mutex);
ESP_LOGI(TAG, "Time set manually to %ld", current_time);
return ESP_OK;
}
esp_err_t scheduler_sync_time_ntp(void)
{
if (esp_sntp_get_sync_status() == SNTP_SYNC_STATUS_IN_PROGRESS) {
return ESP_ERR_NOT_FINISHED;
}
// Trigger sync
esp_sntp_restart();
return ESP_OK;
}
bool scheduler_is_time_synchronized(void)
{
return s_scheduler.time_synchronized;
}
time_t scheduler_get_current_time(void)
{
time_t now;
time(&now);
return now;
}
esp_err_t scheduler_set_holiday_mode(bool enabled)
{
xSemaphoreTake(s_scheduler.mutex, portMAX_DELAY);
s_scheduler.holiday_mode = enabled;
esp_err_t ret = save_global_settings();
xSemaphoreGive(s_scheduler.mutex);
ESP_LOGI(TAG, "Holiday mode %s", enabled ? "enabled" : "disabled");
return ret;
}
bool scheduler_get_holiday_mode(void)
{
return s_scheduler.holiday_mode;
}
esp_err_t scheduler_get_status(scheduler_status_t *status)
{
if (!status) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_scheduler.mutex, portMAX_DELAY);
status->holiday_mode = s_scheduler.holiday_mode;
status->time_synchronized = s_scheduler.time_synchronized;
status->last_sync_time = s_scheduler.last_sync_time;
// Count active schedules
status->active_schedules = 0;
for (int pump = 0; pump < SCHEDULER_MAX_PUMPS; pump++) {
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
if (s_scheduler.schedules[pump][sched].enabled &&
s_scheduler.schedules[pump][sched].type != SCHEDULE_TYPE_DISABLED) {
status->active_schedules++;
}
}
}
xSemaphoreGive(s_scheduler.mutex);
return ESP_OK;
}
time_t scheduler_calculate_next_run(const schedule_config_t *config, time_t from_time)
{
if (!config || config->type == SCHEDULE_TYPE_DISABLED || !config->enabled) {
return 0;
}
struct tm timeinfo;
localtime_r(&from_time, &timeinfo);
switch (config->type) {
case SCHEDULE_TYPE_INTERVAL:
// Simple interval from last run or from now
if (config->last_run > 0) {
return config->last_run + (config->interval_minutes * 60);
} else {
return from_time + (config->interval_minutes * 60);
}
case SCHEDULE_TYPE_TIME_OF_DAY:
{
// Daily at specific time
struct tm next_time = timeinfo;
next_time.tm_hour = config->hour;
next_time.tm_min = config->minute;
next_time.tm_sec = 0;
time_t next_run = mktime(&next_time);
// If time has passed today, schedule for tomorrow
if (next_run <= from_time) {
next_time.tm_mday++;
next_run = mktime(&next_time);
}
return next_run;
}
case SCHEDULE_TYPE_DAYS_TIME:
{
// Specific days at specific time
struct tm next_time = timeinfo;
next_time.tm_hour = config->hour;
next_time.tm_min = config->minute;
next_time.tm_sec = 0;
// Find next matching day
for (int days_ahead = 0; days_ahead < 8; days_ahead++) {
struct tm check_time = next_time;
check_time.tm_mday += days_ahead;
time_t check_timestamp = mktime(&check_time);
localtime_r(&check_timestamp, &check_time);
// Check if this day matches our mask
uint8_t day_bit = (1 << check_time.tm_wday);
if ((config->days_mask & day_bit) && check_timestamp > from_time) {
return check_timestamp;
}
}
return 0; // No matching day found (shouldn't happen with valid mask)
}
default:
return 0;
}
}
// Task that checks and executes schedules
static void scheduler_task(void *pvParameters)
{
ESP_LOGI(TAG, "Scheduler task started");
while (1) {
// Wait 30 seconds between checks
vTaskDelay(pdMS_TO_TICKS(30000));
if (!s_scheduler.time_synchronized) {
ESP_LOGD(TAG, "Waiting for time synchronization...");
continue;
}
if (s_scheduler.holiday_mode) {
ESP_LOGD(TAG, "Holiday mode active, skipping schedules");
continue;
}
check_and_execute_schedules();
}
}
static void check_and_execute_schedules(void)
{
time_t now = scheduler_get_current_time();
xSemaphoreTake(s_scheduler.mutex, portMAX_DELAY);
for (int pump = 0; pump < SCHEDULER_MAX_PUMPS; pump++) {
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
schedule_config_t *schedule = &s_scheduler.schedules[pump][sched];
if (!schedule->enabled || schedule->type == SCHEDULE_TYPE_DISABLED) {
continue;
}
// Check if it's time to run
if (should_run_now(schedule, now)) {
ESP_LOGI(TAG, "Triggering schedule %d for pump %d", sched, pump + 1);
// Update last run time
schedule->last_run = now;
// Calculate next run time
schedule->next_run = scheduler_calculate_next_run(schedule, now);
// Save updated schedule
save_schedule_to_nvs(pump + 1, sched);
// Call trigger callback
if (s_scheduler.trigger_callback) {
s_scheduler.trigger_callback(pump + 1, sched,
schedule->duration_ms,
schedule->speed_percent);
}
}
}
}
xSemaphoreGive(s_scheduler.mutex);
}
static bool should_run_now(const schedule_config_t *config, time_t current_time)
{
if (!config || !config->enabled || config->type == SCHEDULE_TYPE_DISABLED) {
return false;
}
// Don't run if we've run in the last minute (prevent double triggers)
if (config->last_run > 0 && (current_time - config->last_run) < 60) {
return false;
}
switch (config->type) {
case SCHEDULE_TYPE_INTERVAL:
// Check if interval has elapsed
if (config->last_run == 0) {
// First run
return true;
}
return (current_time - config->last_run) >= (config->interval_minutes * 60);
case SCHEDULE_TYPE_TIME_OF_DAY:
case SCHEDULE_TYPE_DAYS_TIME:
// Check if we're within a minute of the scheduled time
if (config->next_run > 0 &&
current_time >= config->next_run &&
current_time < (config->next_run + 60)) {
return true;
}
break;
case SCHEDULE_TYPE_DISABLED:
case SCHEDULE_TYPE_MAX:
default:
// Should never reach here due to initial check, but needed for compiler
break;
}
return false;
}
// JSON serialization
esp_err_t scheduler_schedule_to_json(uint8_t pump_id, uint8_t schedule_id,
char *buffer, size_t buffer_size)
{
if (!buffer || buffer_size == 0) {
return ESP_ERR_INVALID_ARG;
}
schedule_config_t config;
esp_err_t ret = scheduler_get_schedule(pump_id, schedule_id, &config);
if (ret != ESP_OK) {
return ret;
}
// Build JSON manually without cJSON library
int written = 0;
written = snprintf(buffer, buffer_size,
"{\"pump_id\":%d,\"schedule_id\":%d,\"type\":\"%s\",\"enabled\":%s,",
pump_id, schedule_id,
scheduler_get_type_string(config.type),
config.enabled ? "true" : "false");
// Add type-specific fields
switch (config.type) {
case SCHEDULE_TYPE_INTERVAL:
written += snprintf(buffer + written, buffer_size - written,
"\"interval_minutes\":%lu,", config.interval_minutes);
break;
case SCHEDULE_TYPE_TIME_OF_DAY:
written += snprintf(buffer + written, buffer_size - written,
"\"hour\":%d,\"minute\":%d,", config.hour, config.minute);
break;
case SCHEDULE_TYPE_DAYS_TIME:
{
char days_str[64];
scheduler_get_days_string(config.days_mask, days_str, sizeof(days_str));
written += snprintf(buffer + written, buffer_size - written,
"\"hour\":%d,\"minute\":%d,\"days_mask\":%d,\"days\":\"%s\",",
config.hour, config.minute, config.days_mask, days_str);
break;
}
case SCHEDULE_TYPE_DISABLED:
case SCHEDULE_TYPE_MAX:
default:
// No additional fields for disabled type
break;
}
// Add common fields
written += snprintf(buffer + written, buffer_size - written,
"\"duration_ms\":%lu,\"speed_percent\":%d",
config.duration_ms, config.speed_percent);
// Add runtime info if available
if (config.last_run > 0) {
written += snprintf(buffer + written, buffer_size - written,
",\"last_run\":%lld", (long long)config.last_run);
}
if (config.next_run > 0) {
struct tm timeinfo;
localtime_r(&config.next_run, &timeinfo);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &timeinfo);
written += snprintf(buffer + written, buffer_size - written,
",\"next_run\":%lld,\"next_run_str\":\"%s\"",
(long long)config.next_run, time_str);
}
// Close JSON
written += snprintf(buffer + written, buffer_size - written, "}");
return (written < buffer_size) ? ESP_OK : ESP_ERR_INVALID_SIZE;
}
esp_err_t scheduler_json_to_schedule(const char *json, uint8_t pump_id, uint8_t schedule_id)
{
if (!json) {
return ESP_ERR_INVALID_ARG;
}
schedule_config_t config = {0};
// Simple JSON parsing without cJSON
// Look for key patterns in the JSON string
const char *p;
// Parse type
p = strstr(json, "\"type\":");
if (p) {
p += 7; // Skip "type":
while (*p == ' ' || *p == '"') p++;
if (strncmp(p, "disabled", 8) == 0) {
config.type = SCHEDULE_TYPE_DISABLED;
} else if (strncmp(p, "interval", 8) == 0) {
config.type = SCHEDULE_TYPE_INTERVAL;
} else if (strncmp(p, "time_of_day", 11) == 0) {
config.type = SCHEDULE_TYPE_TIME_OF_DAY;
} else if (strncmp(p, "days_time", 9) == 0) {
config.type = SCHEDULE_TYPE_DAYS_TIME;
}
}
// Parse enabled
p = strstr(json, "\"enabled\":");
if (p) {
p += 10;
while (*p == ' ') p++;
config.enabled = (strncmp(p, "true", 4) == 0);
}
// Parse interval_minutes for interval type
if (config.type == SCHEDULE_TYPE_INTERVAL) {
p = strstr(json, "\"interval_minutes\":");
if (p) {
p += 19;
config.interval_minutes = atoi(p);
}
}
// Parse hour and minute for time-based types
if (config.type == SCHEDULE_TYPE_TIME_OF_DAY || config.type == SCHEDULE_TYPE_DAYS_TIME) {
p = strstr(json, "\"hour\":");
if (p) {
p += 7;
config.hour = atoi(p);
}
p = strstr(json, "\"minute\":");
if (p) {
p += 9;
config.minute = atoi(p);
}
// Parse days_mask for days_time type
if (config.type == SCHEDULE_TYPE_DAYS_TIME) {
p = strstr(json, "\"days_mask\":");
if (p) {
p += 12;
config.days_mask = atoi(p);
}
}
}
// Parse duration_ms
p = strstr(json, "\"duration_ms\":");
if (p) {
p += 14;
config.duration_ms = atoi(p);
}
// Parse speed_percent
p = strstr(json, "\"speed_percent\":");
if (p) {
p += 16;
config.speed_percent = atoi(p);
}
// Add the schedule
return scheduler_add_schedule(pump_id, schedule_id, &config);
}
// NVS persistence
static esp_err_t save_schedule_to_nvs(uint8_t pump_id, uint8_t schedule_id)
{
nvs_handle_t nvs_handle;
esp_err_t ret = nvs_open(SCHEDULER_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
if (ret != ESP_OK) {
return ret;
}
char key[32];
snprintf(key, sizeof(key), "sched_%d_%d", pump_id, schedule_id);
// Don't save runtime fields
schedule_config_t config = s_scheduler.schedules[pump_id - 1][schedule_id];
config.last_run = 0;
config.next_run = 0;
ret = nvs_set_blob(nvs_handle, key, &config, sizeof(schedule_config_t));
if (ret == ESP_OK) {
nvs_commit(nvs_handle);
}
nvs_close(nvs_handle);
return ret;
}
static esp_err_t load_schedule_from_nvs(uint8_t pump_id, uint8_t schedule_id)
{
nvs_handle_t nvs_handle;
esp_err_t ret = nvs_open(SCHEDULER_NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
if (ret != ESP_OK) {
return ret;
}
char key[32];
snprintf(key, sizeof(key), "sched_%d_%d", pump_id, schedule_id);
size_t length = sizeof(schedule_config_t);
ret = nvs_get_blob(nvs_handle, key, &s_scheduler.schedules[pump_id - 1][schedule_id], &length);
nvs_close(nvs_handle);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Loaded schedule %d for pump %d from NVS", schedule_id, pump_id);
}
return ret;
}
static esp_err_t save_global_settings(void)
{
nvs_handle_t nvs_handle;
esp_err_t ret = nvs_open(SCHEDULER_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
if (ret != ESP_OK) {
return ret;
}
ret = nvs_set_u8(nvs_handle, "holiday_mode", s_scheduler.holiday_mode ? 1 : 0);
if (ret == ESP_OK) {
nvs_commit(nvs_handle);
}
nvs_close(nvs_handle);
return ret;
}
static esp_err_t load_global_settings(void)
{
nvs_handle_t nvs_handle;
esp_err_t ret = nvs_open(SCHEDULER_NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
if (ret != ESP_OK) {
return ret;
}
uint8_t holiday_mode = 0;
ret = nvs_get_u8(nvs_handle, "holiday_mode", &holiday_mode);
if (ret == ESP_OK) {
s_scheduler.holiday_mode = (holiday_mode != 0);
}
nvs_close(nvs_handle);
return ret;
}
// Utility functions
const char* scheduler_get_type_string(schedule_type_t type)
{
switch (type) {
case SCHEDULE_TYPE_DISABLED: return "disabled";
case SCHEDULE_TYPE_INTERVAL: return "interval";
case SCHEDULE_TYPE_TIME_OF_DAY: return "time_of_day";
case SCHEDULE_TYPE_DAYS_TIME: return "days_time";
default: return "unknown";
}
}
const char* scheduler_get_days_string(uint8_t days_mask, char *buffer, size_t size)
{
if (!buffer || size == 0) {
return "";
}
buffer[0] = '\0';
if (days_mask == SCHEDULE_DAY_ALL) {
strlcpy(buffer, "Daily", size);
return buffer;
}
if (days_mask == SCHEDULE_DAY_WEEKDAYS) {
strlcpy(buffer, "Weekdays", size);
return buffer;
}
if (days_mask == SCHEDULE_DAY_WEEKEND) {
strlcpy(buffer, "Weekends", size);
return buffer;
}
// Build custom day string
const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
bool first = true;
for (int i = 0; i < 7; i++) {
if (days_mask & (1 << i)) {
if (!first) {
strlcat(buffer, ",", size);
}
strlcat(buffer, days[i], size);
first = false;
}
}
return buffer;
}
// Callbacks
void scheduler_register_trigger_callback(schedule_trigger_callback_t callback)
{
s_scheduler.trigger_callback = callback;
}
void scheduler_register_status_callback(schedule_status_callback_t callback)
{
s_scheduler.status_callback = callback;
}
// Manual trigger for testing
esp_err_t scheduler_trigger_schedule(uint8_t pump_id, uint8_t schedule_id)
{
if (!s_scheduler.initialized) {
return ESP_ERR_INVALID_STATE;
}
if (pump_id < 1 || pump_id > SCHEDULER_MAX_PUMPS ||
schedule_id >= SCHEDULER_MAX_SCHEDULES_PER_PUMP) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_scheduler.mutex, portMAX_DELAY);
schedule_config_t *schedule = &s_scheduler.schedules[pump_id - 1][schedule_id];
if (schedule->type == SCHEDULE_TYPE_DISABLED || !schedule->enabled) {
xSemaphoreGive(s_scheduler.mutex);
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "Manual trigger of schedule %d for pump %d", schedule_id, pump_id);
// Call trigger callback
if (s_scheduler.trigger_callback) {
s_scheduler.trigger_callback(pump_id, schedule_id,
schedule->duration_ms,
schedule->speed_percent);
}
xSemaphoreGive(s_scheduler.mutex);
return ESP_OK;
}

124
main/scheduler.h Normal file
View File

@ -0,0 +1,124 @@
#ifndef SCHEDULER_H
#define SCHEDULER_H
#include <stdbool.h>
#include <stdint.h>
#include <time.h>
#include "esp_err.h"
// Maximum number of schedules per pump
#define SCHEDULER_MAX_SCHEDULES_PER_PUMP 4
#define SCHEDULER_MAX_PUMPS 2
// Schedule types
typedef enum {
SCHEDULE_TYPE_DISABLED = 0,
SCHEDULE_TYPE_INTERVAL, // Every X minutes
SCHEDULE_TYPE_TIME_OF_DAY, // Daily at specific time
SCHEDULE_TYPE_DAYS_TIME, // Specific days at specific time
SCHEDULE_TYPE_MAX
} schedule_type_t;
// Days of week bitmask (bit 0 = Sunday, bit 6 = Saturday)
#define SCHEDULE_DAY_SUNDAY (1 << 0)
#define SCHEDULE_DAY_MONDAY (1 << 1)
#define SCHEDULE_DAY_TUESDAY (1 << 2)
#define SCHEDULE_DAY_WEDNESDAY (1 << 3)
#define SCHEDULE_DAY_THURSDAY (1 << 4)
#define SCHEDULE_DAY_FRIDAY (1 << 5)
#define SCHEDULE_DAY_SATURDAY (1 << 6)
#define SCHEDULE_DAY_WEEKDAYS (SCHEDULE_DAY_MONDAY | SCHEDULE_DAY_TUESDAY | \
SCHEDULE_DAY_WEDNESDAY | SCHEDULE_DAY_THURSDAY | \
SCHEDULE_DAY_FRIDAY)
#define SCHEDULE_DAY_WEEKEND (SCHEDULE_DAY_SATURDAY | SCHEDULE_DAY_SUNDAY)
#define SCHEDULE_DAY_ALL 0x7F
// Schedule configuration
typedef struct {
schedule_type_t type;
bool enabled;
// Timing configuration
uint32_t interval_minutes; // For SCHEDULE_TYPE_INTERVAL
uint8_t hour; // For TIME_OF_DAY and DAYS_TIME (0-23)
uint8_t minute; // For TIME_OF_DAY and DAYS_TIME (0-59)
uint8_t days_mask; // For DAYS_TIME (bitmask)
// Watering configuration
uint32_t duration_ms; // How long to water (milliseconds)
uint8_t speed_percent; // Pump speed (0-100)
// Runtime info (not saved to NVS)
time_t last_run; // Last execution timestamp
time_t next_run; // Next scheduled run
} schedule_config_t;
// Schedule entry with ID
typedef struct {
uint8_t pump_id; // Which pump (1 or 2)
uint8_t schedule_id; // Schedule slot (0-3)
schedule_config_t config; // Schedule configuration
} schedule_entry_t;
// Schedule status
typedef struct {
bool holiday_mode; // Global disable for all schedules
bool time_synchronized; // Whether we have valid time
time_t last_sync_time; // When time was last synchronized
uint32_t active_schedules; // Number of active schedules
} scheduler_status_t;
// Callbacks
typedef void (*schedule_trigger_callback_t)(uint8_t pump_id, uint8_t schedule_id,
uint32_t duration_ms, uint8_t speed_percent);
typedef void (*schedule_status_callback_t)(const char* status_json);
// Scheduler functions
esp_err_t scheduler_init(void);
esp_err_t scheduler_deinit(void);
// Schedule management
esp_err_t scheduler_add_schedule(uint8_t pump_id, uint8_t schedule_id,
const schedule_config_t *config);
esp_err_t scheduler_get_schedule(uint8_t pump_id, uint8_t schedule_id,
schedule_config_t *config);
esp_err_t scheduler_remove_schedule(uint8_t pump_id, uint8_t schedule_id);
esp_err_t scheduler_enable_schedule(uint8_t pump_id, uint8_t schedule_id, bool enable);
esp_err_t scheduler_clear_all_schedules(void);
// Time management
esp_err_t scheduler_set_time(time_t current_time);
esp_err_t scheduler_sync_time_ntp(void);
bool scheduler_is_time_synchronized(void);
time_t scheduler_get_current_time(void);
// Holiday mode
esp_err_t scheduler_set_holiday_mode(bool enabled);
bool scheduler_get_holiday_mode(void);
// Status and information
esp_err_t scheduler_get_status(scheduler_status_t *status);
esp_err_t scheduler_get_next_run_times(time_t *next_runs, size_t max_count);
esp_err_t scheduler_get_all_schedules(schedule_entry_t *entries, size_t max_entries,
size_t *count);
// JSON serialization for MQTT
esp_err_t scheduler_schedule_to_json(uint8_t pump_id, uint8_t schedule_id,
char *buffer, size_t buffer_size);
esp_err_t scheduler_json_to_schedule(const char *json, uint8_t pump_id,
uint8_t schedule_id);
esp_err_t scheduler_status_to_json(char *buffer, size_t buffer_size);
// Callbacks
void scheduler_register_trigger_callback(schedule_trigger_callback_t callback);
void scheduler_register_status_callback(schedule_status_callback_t callback);
// Manual trigger (for testing)
esp_err_t scheduler_trigger_schedule(uint8_t pump_id, uint8_t schedule_id);
// Utility functions
const char* scheduler_get_type_string(schedule_type_t type);
const char* scheduler_get_days_string(uint8_t days_mask, char *buffer, size_t size);
time_t scheduler_calculate_next_run(const schedule_config_t *config, time_t from_time);
#endif // SCHEDULER_H

306
project_plan_v2.md Normal file
View File

@ -0,0 +1,306 @@
# ESP32-S3 Plant Watering System - Project Plan v2.0
*Updated: January 2025*
## Project Overview
**Goal**: Automated plant watering system with remote monitoring/control
**Hardware**: ESP32-S3-MINI-1, TB6612FNG motor driver, 2 pumps, 2 soil moisture sensors
**Software**: ESP-IDF v6, MQTT communication, OTA updates, NTP time sync, Scheduling
**Infrastructure**: Docker-based Mosquitto MQTT broker, local network deployment
## Project Status Summary
- **Phase 1**: Core Infrastructure ✅ COMPLETED
- **Phase 2**: Hardware Integration 🚧 75% COMPLETE (Motors done, Sensors pending)
- **Phase 3**: Automation Logic ✅ COMPLETED (via Scheduler)
- **Phase 4**: Enhanced Features 🚧 50% COMPLETE
- **Phase 5**: Production Polish 📋 TODO
---
## Phase 1: Core Infrastructure ✅ COMPLETED
### 1.1 Development Environment ✅
- ESP-IDF v6 in Docker container
- Build system configured
- Project structure created
### 1.2 WiFi Manager ✅
- Auto-connect to configured network
- Credential storage in NVS
- Auto-reconnection on disconnect
- Event callbacks for state changes
### 1.3 OTA Server ✅
- HTTP server on port 80
- Web interface for firmware upload
- Progress tracking
- Version management
- Rollback capability
### 1.4 MQTT Client ✅
- Connection with authentication
- Auto-reconnect with exponential backoff
- Last Will and Testament
- NVS credential storage
- Comprehensive publish/subscribe implementation
- Topics implemented:
- `plant_watering/status`
- `plant_watering/moisture/[1-2]`
- `plant_watering/pump/[1-2]/set`
- `plant_watering/pump/[1-2]/state`
- `plant_watering/pump/[1-2]/speed`
- `plant_watering/schedule/*`
- `plant_watering/system/*`
- `plant_watering/commands/*`
---
## Phase 2: Hardware Integration 🚧 75% COMPLETE
### 2.1 Motor Control Module ✅ COMPLETED
**Files**: `motor_control.c/h`
- TB6612FNG driver implementation ✅
- PWM control for pump speed ✅
- Direction control ✅
- Safety features:
- Maximum runtime limit ✅
- Minimum interval between runs ✅
- Soft start (500ms ramp) ✅
- Emergency stop ✅
- Manual override via MQTT ✅
- State tracking and reporting ✅
- Runtime statistics with NVS persistence ✅
- Tested and working with hardware ✅
### 2.2 Moisture Sensor Module ⏸️ ON HOLD
**File**: `moisture_sensor.c/h`
- [ ] ADC configuration for 2 sensors
- [ ] Calibration system
- [ ] Reading stabilization
- [ ] Percentage conversion
- [ ] Sensor fault detection
**Note**: Awaiting hardware delivery
### 2.3 Hardware Integration Testing 🚧 PARTIAL
- [x] Pump operation verified
- [x] PWM speed control tested
- [x] Safety features validated
- [ ] Moisture sensor integration (pending hardware)
- [ ] Full system integration test
---
## Phase 3: Automation Logic ✅ COMPLETED (via Scheduler)
### 3.1 Time-Based Scheduling ✅ COMPLETED
**Files**: `scheduler.c/h`
- Multiple schedule types:
- Interval-based (every X minutes) ✅
- Time of day (daily at specific time) ✅
- Day-specific (specific days at time) ✅
- Per-pump scheduling (4 schedules each) ✅
- NVS persistence ✅
- MQTT configuration ✅
- Holiday mode ✅
- Manual trigger for testing ✅
### 3.2 Time Synchronization ✅ COMPLETED
- NTP client implementation ✅
- Multiple NTP servers ✅
- Manual time setting fallback ✅
- Timezone support (MST default) ✅
- Time status reporting ✅
### 3.3 Schedule Management ✅ COMPLETED
- Create/update schedules via MQTT ✅
- View all schedules on demand ✅
- Enable/disable without deletion ✅
- Execution notifications ✅
- Error reporting ✅
### 3.4 Moisture-Based Automation ⏸️ PENDING
- Awaiting sensor hardware
- Will integrate with scheduler when available
---
## Phase 4: Enhanced MQTT & Monitoring 🚧 50% COMPLETE
### 4.1 Expanded MQTT Topics ✅ COMPLETED
Comprehensive topic structure implemented:
```
plant_watering/
├── status/esp32/*
├── pump/[1-2]/*
├── schedule/*
├── system/*
├── settings/*
├── alerts/*
└── commands/*
```
### 4.2 JSON Payloads ✅ COMPLETED
- Structured data for schedules ✅
- Status messages ✅
- Configuration updates ✅
- Built-in JSON parsing (no external deps) ✅
### 4.3 Alert System 🚧 PARTIAL
- [x] Pump malfunction alerts
- [x] Schedule execution errors
- [ ] Low moisture alerts (pending sensors)
- [ ] Water tank monitoring (future)
### 4.4 Remote Configuration ✅ COMPLETED
- MQTT-based settings updates ✅
- Validation and bounds checking ✅
- Persistent storage in NVS ✅
- Runtime parameter changes ✅
---
## Phase 5: Production Features 📋 TODO
### 5.1 Web Dashboard 📋 TODO
- [ ] Real-time status display
- [ ] Historical graphs
- [ ] Manual control interface
- [ ] Schedule configuration UI
- [ ] Mobile-responsive design
### 5.2 Home Assistant Integration 📋 TODO
- [ ] MQTT Discovery implementation
- [ ] Device registry
- [ ] Entity configuration
- [ ] Automation examples
### 5.3 Advanced Features 📋 TODO
- [ ] Multi-zone support (>2 zones)
- [ ] Flow sensor integration
- [ ] Weather API integration
- [ ] Predictive watering
- [ ] Water usage tracking
### 5.4 Production Hardening 🚧 PARTIAL
- [x] Watchdog timer (system level)
- [ ] Enhanced error recovery
- [ ] Factory reset mechanism
- [ ] Diagnostic mode
- [x] Memory monitoring
---
## Current Capabilities
### Working Features
1. **Remote Control**: Full MQTT control of pumps
2. **Scheduling**: Time-based watering with multiple schedule types
3. **Safety**: Runtime limits, cooldown periods, emergency stop
4. **Monitoring**: Real-time status, statistics, time sync status
5. **OTA Updates**: Web-based firmware updates
6. **Persistence**: Settings and schedules survive reboots
7. **Time Management**: NTP sync with manual fallback
### Tested and Verified
- Motor control with PWM speed adjustment
- Schedule execution (time_of_day and days_time modes)
- MQTT command processing
- Safety features (max runtime, min interval)
- OTA firmware updates
- NVS persistence
- Time synchronization
---
## Hardware Connections (Reference)
### ESP32-S3 to TB6612FNG ✅ VERIFIED
```
ESP32-S3 TB6612FNG Status
GPIO4 -> AIN1 ✅ Working
GPIO5 -> AIN2 ✅ Working
GPIO6 -> BIN1 ✅ Working
GPIO7 -> BIN2 ✅ Working
GPIO8 -> PWMA ✅ Working
GPIO9 -> PWMB ✅ Working
GPIO10 -> STBY ✅ Working
GND -> GND ✅ Connected
3.3V -> VCC ✅ Connected
```
### Moisture Sensors ⏸️ PENDING
```
Sensor 1 -> GPIO1 (ADC1_CHANNEL_0) - Awaiting hardware
Sensor 2 -> GPIO2 (ADC1_CHANNEL_1) - Awaiting hardware
```
---
## Next Steps
### Immediate (This Week)
1.~~Test interval scheduling mode~~
2. ⏸️ Install moisture sensors when they arrive
3.~~Document all MQTT topics comprehensively~~
4. ⏸️ Create basic web dashboard mockup
### Short Term (Next Month)
1. Implement moisture sensor module
2. Integrate moisture readings with scheduler
3. Add moisture-based automation rules
4. Implement Home Assistant discovery
### Long Term
1. Multi-zone expansion (>2 pumps)
2. Weather API integration
3. Water usage analytics
4. Mobile app development
---
## Success Metrics
- ✅ Reliable WiFi/MQTT connectivity (achieved)
- ✅ Accurate time-based scheduling (achieved)
- ✅ Precise watering control (achieved)
- ✅ OTA updates without service interruption (achieved)
- ✅ Response time < 1 second for commands (achieved)
- Accurate moisture readings 5%) - pending hardware
- 30-day uptime without intervention - in progress
- < 100mA average power consumption - to be measured
---
## Known Issues
1. None currently - system stable
## Risk Mitigation
- Implemented watchdog timers
- Added redundant safety checks
- Local schedule storage (survives network loss)
- Comprehensive error logging
- Sensor corrosion prevention (pending hardware)
---
## Version History
- **v2.2.0-scheduler**: Full scheduling system with NTP
- **v2.1.0-motor**: Motor control implementation
- **v2.0.0-mqtt**: MQTT integration
- **v1.0.1**: OTA-enabled base
- **v1.0.0**: Initial template
---
## Documentation Status
- README.md - Comprehensive project overview
- MOTOR_CONTROL_README.md - Motor control details
- SCHEDULER_README.md - Scheduling system guide
- MQTT_TOPICS.md - To be created
- API_REFERENCE.md - To be created
---
## Notes
- Scheduler tested with days_time mode - working correctly
- Time sync typically completes within 30 seconds of boot
- System handles DST transitions automatically
- All safety features have been validated with hardware