Compare commits

8 Commits

14 changed files with 4321 additions and 31 deletions

267
MOTOR_CONTROL_README.md Normal file
View File

@ -0,0 +1,267 @@
# Motor Control Module
This module provides safe and reliable control of water pumps using the TB6612FNG motor driver.
## Features
- **Dual Motor Control**: Independent control of 2 DC water pumps
- **PWM Speed Control**: Variable speed from 20% to 100%
- **Safety Features**:
- Maximum runtime protection (default 30 seconds)
- Minimum interval between runs (default 5 minutes)
- Soft-start to reduce current spikes
- Emergency stop functionality
- **Runtime Statistics**: Track usage, runtime, and error counts
- **MQTT Integration**: Full remote control and monitoring
- **NVS Persistence**: Statistics survive reboots
## Hardware Connections
### ESP32-S3 to TB6612FNG Wiring
| ESP32-S3 | TB6612FNG | Function |
|----------|-----------|----------|
| GPIO4 | AIN1 | Pump 1 Direction |
| GPIO5 | AIN2 | Pump 1 Direction |
| GPIO6 | BIN1 | Pump 2 Direction |
| GPIO7 | BIN2 | Pump 2 Direction |
| GPIO8 | PWMA | Pump 1 Speed (PWM) |
| GPIO9 | PWMB | Pump 2 Speed (PWM) |
| GPIO10 | STBY | Standby (Active High) |
| GND | GND | Ground |
| 3.3V | VCC | Logic Power |
### Power Connections
- **VM** on TB6612FNG: Connect to pump power supply (12V typical)
- **Pump 1**: Connect to AOUT1 and AOUT2
- **Pump 2**: Connect to BOUT1 and BOUT2
## Usage
### Basic Control
```c
// Initialize the motor control system
motor_control_init();
// Start pump at default speed (80%)
motor_start(MOTOR_PUMP_1, MOTOR_DEFAULT_SPEED);
// Start pump at specific speed
motor_start(MOTOR_PUMP_2, 60); // 60% speed
// Stop pumps
motor_stop(MOTOR_PUMP_1);
motor_stop_all(); // Stop all pumps
// Emergency stop (immediate)
motor_emergency_stop();
```
### Timed Operations
```c
// Run pump for specific duration
motor_start_timed(MOTOR_PUMP_1, 80, 10000); // 80% speed for 10 seconds
// Pulse operation
motor_pulse(MOTOR_PUMP_1, 90, 2000, 1000, 5); // On 2s, off 1s, repeat 5x
```
### Speed Control
```c
// Change speed while running
motor_set_speed(MOTOR_PUMP_1, 50); // Change to 50%
// Set speed limits
motor_set_speed_limits(MOTOR_PUMP_1, 30, 90); // Min 30%, Max 90%
```
### Safety Configuration
```c
// Set maximum runtime (prevents pump from running too long)
motor_set_max_runtime(MOTOR_PUMP_1, 60000); // 60 seconds max
// Set minimum interval between runs (prevents frequent cycling)
motor_set_min_interval(MOTOR_PUMP_1, 300000); // 5 minutes
```
### Status and Statistics
```c
// Check if pump is running
if (motor_is_running(MOTOR_PUMP_1)) {
uint32_t runtime = motor_get_runtime_ms(MOTOR_PUMP_1);
ESP_LOGI(TAG, "Pump has been running for %d ms", runtime);
}
// Check if in cooldown
if (motor_is_cooldown(MOTOR_PUMP_1)) {
ESP_LOGI(TAG, "Pump is in cooldown period");
}
// Get statistics
motor_stats_t stats;
motor_get_stats(MOTOR_PUMP_1, &stats);
ESP_LOGI(TAG, "Total runtime: %d seconds", stats.total_runtime_ms / 1000);
ESP_LOGI(TAG, "Total runs: %d", stats.run_count);
```
## MQTT Commands
### Basic Control
- **Topic**: `plant_watering/pump/[1-2]/set`
- **Payload**:
- `on` - Start pump at default speed
- `off` - Stop pump
- `pulse` - Run pump for 5 seconds
### Speed Control
- **Topic**: `plant_watering/pump/[1-2]/speed`
- **Payload**: `0-100` (percentage)
### Test Commands
- **Topic**: `plant_watering/commands/test_pump/[1-2]`
- **Payload**: Duration in milliseconds (max 10000)
### Emergency Stop
- **Topic**: `plant_watering/commands/emergency_stop`
- **Payload**: Any value
## MQTT Status Publishing
The system publishes the following status information:
### Pump State
- **Topic**: `plant_watering/pump/[1-2]/state`
- **Values**: `on`, `off`
### Runtime (when running)
- **Topic**: `plant_watering/pump/[1-2]/runtime`
- **Value**: Current runtime in milliseconds
### Statistics (on connect and periodically)
- **Topic**: `plant_watering/pump/[1-2]/stats`
- **Format**: JSON
```json
{
"total_runtime": 123456,
"run_count": 42,
"last_duration": 5000
}
```
### Errors
- **Topic**: `plant_watering/alerts/pump_error/[1-2]`
- **Value**: Error description string
## Testing
### Hardware Test Program
A standalone test program is provided in `motor_test.c`. To use it:
1. Replace `app_main()` in your main.c with the test version
2. Build and flash
3. Monitor serial output
4. Verify each pump responds correctly
### Test Sequence
1. Individual pump ON/OFF test
2. PWM speed ramping
3. Timed operations
4. Dual pump operation
5. Safety features (cooldown, max runtime)
6. Emergency stop
7. Statistics verification
### Manual Testing via MQTT
```bash
# Start pump 1
mosquitto_pub -h <broker> -t "plant_watering/pump/1/set" -m "on"
# Change speed
mosquitto_pub -h <broker> -t "plant_watering/pump/1/speed" -m "50"
# Stop pump
mosquitto_pub -h <broker> -t "plant_watering/pump/1/set" -m "off"
# Test run for 3 seconds
mosquitto_pub -h <broker> -t "plant_watering/commands/test_pump/1" -m "3000"
# Emergency stop all
mosquitto_pub -h <broker> -t "plant_watering/commands/emergency_stop" -m "1"
```
## Troubleshooting
### Pump Not Starting
1. Check cooldown period hasn't been violated
2. Verify power connections (12V to VM)
3. Check STBY pin is HIGH
4. Verify PWM signal on oscilloscope
### Pump Runs Continuously
1. Check safety timer is working
2. Verify MQTT commands are being received
3. Check for stuck relay/MOSFET
### Low Power/Speed
1. Check power supply voltage and current capacity
2. Verify PWM duty cycle
3. Check for voltage drop in wiring
4. Ensure pumps aren't clogged
### Error Messages
- **"Cooldown period not elapsed"**: Wait for minimum interval
- **"Maximum runtime exceeded"**: Safety timer triggered
- **"Motor not initialized"**: Call `motor_control_init()` first
## Design Considerations
### Soft Start
The module implements a 500ms soft-start sequence, ramping PWM from 0 to target speed in 5% increments. This reduces current spikes and mechanical stress.
### Unidirectional Operation
While the TB6612FNG supports bidirectional control, pumps are configured for forward operation only. The direction pins are set but typically won't be changed.
### Power Management
The STBY pin is used to enable/disable the motor driver. During emergency stop, STBY is pulled low momentarily to ensure immediate motor shutdown.
### Statistics Persistence
Runtime statistics are saved to NVS every 10 pump cycles to minimize flash wear while preserving useful data across reboots.
## Integration Example
```c
// In your main application
void app_main() {
// Initialize subsystems
wifi_manager_init();
mqtt_client_init();
motor_control_init();
// Configure safety limits from Kconfig
motor_set_max_runtime(MOTOR_PUMP_1, CONFIG_WATERING_MAX_DURATION_MS);
motor_set_min_interval(MOTOR_PUMP_1, CONFIG_WATERING_MIN_INTERVAL_MS);
// Register callbacks
motor_register_state_callback(on_motor_state_change);
motor_register_error_callback(on_motor_error);
// Start your application...
}
// Automation example
void water_if_dry() {
if (soil_moisture < 30 && !motor_is_cooldown(MOTOR_PUMP_1)) {
motor_start_timed(MOTOR_PUMP_1, 70, 15000); // 15 seconds at 70%
}
}
```

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

8
cmd.txt Normal file
View File

@ -0,0 +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_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

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

View File

@ -9,18 +9,97 @@
#include "wifi_manager.h"
#include "ota_server.h"
#include "plant_mqtt.h"
#include "motor_control.h"
#include "scheduler.h"
#include "sdkconfig.h"
// Uncomment this line to enable motor test mode with shorter intervals
// #define MOTOR_TEST_MODE
static const char *TAG = "MAIN";
// Application version
#define APP_VERSION "2.0.0-mqtt"
#define APP_VERSION "2.2.0-scheduler"
// Test data
static int test_moisture_1 = 45;
static int test_moisture_2 = 62;
static bool test_pump_1 = false;
static bool test_pump_2 = false;
// Function prototypes
static void print_chip_info(void);
// Motor Control Callbacks
static void motor_state_change_callback(motor_id_t id, motor_state_t state)
{
const char *state_str = "unknown";
switch (state) {
case MOTOR_STATE_STOPPED:
state_str = "off";
break;
case MOTOR_STATE_RUNNING:
state_str = "on";
break;
case MOTOR_STATE_ERROR:
state_str = "error";
break;
case MOTOR_STATE_COOLDOWN:
state_str = "cooldown";
break;
}
ESP_LOGI(TAG, "Motor %d state changed to: %s", id, state_str);
// Publish state change to MQTT
if (mqtt_client_is_connected()) {
mqtt_publish_pump_state(id, state == MOTOR_STATE_RUNNING);
}
}
static void motor_error_callback(motor_id_t id, const char* error)
{
ESP_LOGE(TAG, "Motor %d error: %s", id, error);
// Publish error to MQTT alert topic
if (mqtt_client_is_connected()) {
char topic[64];
snprintf(topic, sizeof(topic), "plant_watering/alerts/pump_error/%d", id);
mqtt_client_publish(topic, error, MQTT_QOS_1, MQTT_NO_RETAIN);
}
}
// 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
static void mqtt_connected_callback(void)
@ -30,8 +109,49 @@ static void mqtt_connected_callback(void)
// Publish initial states
mqtt_publish_moisture(1, test_moisture_1);
mqtt_publish_moisture(2, test_moisture_2);
mqtt_publish_pump_state(1, test_pump_1);
mqtt_publish_pump_state(2, test_pump_2);
mqtt_publish_pump_state(1, motor_is_running(MOTOR_PUMP_1));
mqtt_publish_pump_state(2, motor_is_running(MOTOR_PUMP_2));
// Subscribe to additional topics
static const char* additional_topics[] = {
"plant_watering/pump/+/speed",
"plant_watering/commands/test_pump/+",
"plant_watering/commands/emergency_stop",
"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/+/+",
NULL
};
for (int i = 0; additional_topics[i] != NULL; i++) {
esp_err_t ret = mqtt_client_subscribe(additional_topics[i], MQTT_QOS_1);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Subscribed to: %s", additional_topics[i]);
} else {
ESP_LOGE(TAG, "Failed to subscribe to: %s", additional_topics[i]);
}
}
// Publish motor statistics
motor_stats_t stats;
for (int i = 1; i <= 2; i++) {
if (motor_get_stats(i, &stats) == ESP_OK) {
char topic[64];
char data[128];
snprintf(topic, sizeof(topic), "plant_watering/pump/%d/stats", i);
snprintf(data, sizeof(data),
"{\"total_runtime\":%lu,\"run_count\":%lu,\"last_duration\":%lu}",
stats.total_runtime_ms, stats.run_count, stats.last_run_duration_ms);
mqtt_client_publish(topic, data, MQTT_QOS_0, MQTT_NO_RETAIN);
}
}
}
static void mqtt_disconnected_callback(void)
@ -47,27 +167,245 @@ static void mqtt_data_callback(const char* topic, const char* data, int data_len
// Handle pump control commands
if (strcmp(topic, TOPIC_PUMP_1_CMD) == 0) {
if (strncmp(data, "on", data_len) == 0) {
test_pump_1 = true;
ESP_LOGI(TAG, "Pump 1 turned ON");
mqtt_publish_pump_state(1, test_pump_1);
ESP_LOGI(TAG, "Starting pump 1 via MQTT");
esp_err_t ret = motor_start(MOTOR_PUMP_1, MOTOR_DEFAULT_SPEED);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start pump 1: %s", esp_err_to_name(ret));
}
} else if (strncmp(data, "off", data_len) == 0) {
test_pump_1 = false;
ESP_LOGI(TAG, "Pump 1 turned OFF");
mqtt_publish_pump_state(1, test_pump_1);
ESP_LOGI(TAG, "Stopping pump 1 via MQTT");
motor_stop(MOTOR_PUMP_1);
} else if (strncmp(data, "pulse", data_len) == 0) {
ESP_LOGI(TAG, "Pulse pump 1 for 5 seconds");
motor_start_timed(MOTOR_PUMP_1, MOTOR_DEFAULT_SPEED, 5000);
}
} else if (strcmp(topic, TOPIC_PUMP_2_CMD) == 0) {
if (strncmp(data, "on", data_len) == 0) {
test_pump_2 = true;
ESP_LOGI(TAG, "Pump 2 turned ON");
mqtt_publish_pump_state(2, test_pump_2);
ESP_LOGI(TAG, "Starting pump 2 via MQTT");
esp_err_t ret = motor_start(MOTOR_PUMP_2, MOTOR_DEFAULT_SPEED);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start pump 2: %s", esp_err_to_name(ret));
}
} else if (strncmp(data, "off", data_len) == 0) {
test_pump_2 = false;
ESP_LOGI(TAG, "Pump 2 turned OFF");
mqtt_publish_pump_state(2, test_pump_2);
ESP_LOGI(TAG, "Stopping pump 2 via MQTT");
motor_stop(MOTOR_PUMP_2);
} else if (strncmp(data, "pulse", data_len) == 0) {
ESP_LOGI(TAG, "Pulse pump 2 for 5 seconds");
motor_start_timed(MOTOR_PUMP_2, MOTOR_DEFAULT_SPEED, 5000);
}
} else if (strcmp(topic, "plant_watering/pump/1/speed") == 0) {
int speed = atoi(data);
if (speed >= 0 && speed <= 100) {
motor_set_speed(MOTOR_PUMP_1, speed);
ESP_LOGI(TAG, "Set pump 1 speed to %d%%", speed);
}
} else if (strcmp(topic, "plant_watering/pump/2/speed") == 0) {
int speed = atoi(data);
if (speed >= 0 && speed <= 100) {
motor_set_speed(MOTOR_PUMP_2, speed);
ESP_LOGI(TAG, "Set pump 2 speed to %d%%", speed);
}
} else if (strcmp(topic, TOPIC_CONFIG) == 0) {
ESP_LOGI(TAG, "Configuration update received");
// Parse JSON configuration here
} else if (strcmp(topic, "plant_watering/commands/test_pump/1") == 0) {
uint32_t duration = atoi(data);
if (duration > 0 && duration <= 30000) { // Max 30 seconds for test
ESP_LOGI(TAG, "Test pump 1 for %lu ms", duration);
motor_test_run(MOTOR_PUMP_1, duration);
} else {
ESP_LOGW(TAG, "Invalid test duration: %lu (max 30000ms)", duration);
}
} else if (strcmp(topic, "plant_watering/commands/test_pump/2") == 0) {
uint32_t duration = atoi(data);
if (duration > 0 && duration <= 30000) { // Max 30 seconds for test
ESP_LOGI(TAG, "Test pump 2 for %lu ms", duration);
motor_test_run(MOTOR_PUMP_2, duration);
} else {
ESP_LOGW(TAG, "Invalid test duration: %lu (max 30000ms)", duration);
}
} else if (strcmp(topic, "plant_watering/commands/emergency_stop") == 0) {
ESP_LOGW(TAG, "Emergency stop command received!");
motor_emergency_stop();
} else if (strcmp(topic, "plant_watering/commands/test_mode") == 0) {
if (strncmp(data, "on", data_len) == 0) {
ESP_LOGW(TAG, "Enabling test mode - short intervals");
motor_set_min_interval(MOTOR_PUMP_1, 5000); // 5 seconds
motor_set_min_interval(MOTOR_PUMP_2, 5000);
motor_set_max_runtime(MOTOR_PUMP_1, 30000); // 30 seconds
motor_set_max_runtime(MOTOR_PUMP_2, 30000);
} else if (strncmp(data, "off", data_len) == 0) {
ESP_LOGI(TAG, "Disabling test mode - normal intervals");
motor_set_min_interval(MOTOR_PUMP_1, CONFIG_WATERING_MIN_INTERVAL_MS);
motor_set_min_interval(MOTOR_PUMP_2, CONFIG_WATERING_MIN_INTERVAL_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);
}
} else if (strcmp(topic, "plant_watering/commands/holiday_mode") == 0) {
if (strncmp(data, "on", data_len) == 0) {
scheduler_set_holiday_mode(true);
ESP_LOGI(TAG, "Holiday mode enabled - all schedules paused");
} else if (strncmp(data, "off", data_len) == 0) {
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;
char setting[32] = {0};
// Extract pump ID and setting name
if (sscanf(topic + 29, "%d/%31s", &pump_id, setting) == 2) {
if (pump_id >= 1 && pump_id <= 2) {
int value = atoi(data);
if (strcmp(setting, "max_runtime") == 0 && value > 0) {
motor_set_max_runtime(pump_id, value);
ESP_LOGI(TAG, "Set pump %d max runtime to %d ms", pump_id, value);
} else if (strcmp(setting, "min_interval") == 0 && value > 0) {
motor_set_min_interval(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) {
motor_set_speed_limits(pump_id, value, 100);
ESP_LOGI(TAG, "Set pump %d min speed to %d%%", pump_id, value);
} else if (strcmp(setting, "max_speed") == 0 && value > 0 && value <= 100) {
motor_set_speed_limits(pump_id, MOTOR_MIN_SPEED, value);
ESP_LOGI(TAG, "Set pump %d max speed to %d%%", pump_id, value);
}
}
}
}
}
@ -106,9 +444,11 @@ static void ota_progress_handler(int percent)
ESP_LOGI(TAG, "OTA Progress: %d%%", percent);
}
// Task to simulate sensor readings
// Task to simulate sensor readings and publish stats
static void sensor_simulation_task(void *pvParameters)
{
TickType_t last_stats_publish = 0;
while (1) {
// Wait for MQTT connection
if (mqtt_client_is_connected()) {
@ -128,6 +468,29 @@ static void sensor_simulation_task(void *pvParameters)
ESP_LOGI(TAG, "Published moisture: Sensor1=%d%%, Sensor2=%d%%",
test_moisture_1, test_moisture_2);
// Publish pump runtime stats every minute
if (xTaskGetTickCount() - last_stats_publish > pdMS_TO_TICKS(60000)) {
last_stats_publish = xTaskGetTickCount();
for (int i = 1; i <= 2; i++) {
// Publish current runtime if running
if (motor_is_running(i)) {
char topic[64];
char data[32];
snprintf(topic, sizeof(topic), "plant_watering/pump/%d/runtime", i);
snprintf(data, sizeof(data), "%lu", motor_get_runtime_ms(i));
mqtt_client_publish(topic, data, MQTT_QOS_0, MQTT_NO_RETAIN);
}
// Publish cooldown status
if (motor_is_cooldown(i)) {
char topic[64];
snprintf(topic, sizeof(topic), "plant_watering/pump/%d/cooldown", i);
mqtt_client_publish(topic, "true", MQTT_QOS_0, MQTT_NO_RETAIN);
}
}
}
}
// Update every 10 seconds
@ -135,7 +498,62 @@ static void sensor_simulation_task(void *pvParameters)
}
}
void print_chip_info(void)
// Task to publish schedule status periodically
static void schedule_status_task(void *pvParameters)
{
while (1) {
vTaskDelay(pdMS_TO_TICKS(60000)); // Every minute
if (mqtt_client_is_connected() && scheduler_is_time_synchronized()) {
// Publish scheduler status
scheduler_status_t status;
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);
}
// Publish human-readable time periodically
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 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);
}
}
}
}
}
}
}
static void print_chip_info(void)
{
esp_chip_info_t chip_info;
@ -157,18 +575,18 @@ void app_main(void)
// Print chip information
print_chip_info();
// Print MQTT configuration
ESP_LOGI(TAG, "MQTT Broker: %s", CONFIG_MQTT_BROKER_URL);
ESP_LOGI(TAG, "MQTT Username: %s", CONFIG_MQTT_USERNAME);
// Print 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 high: %d%%", CONFIG_MOISTURE_THRESHOLD_HIGH);
ESP_LOGI(TAG, " Max watering duration: %d ms", CONFIG_WATERING_MAX_DURATION_MS);
ESP_LOGI(TAG, " Min watering interval: %d ms", CONFIG_WATERING_MIN_INTERVAL_MS);
// Initialize WiFi manager
ESP_ERROR_CHECK(wifi_manager_init());
wifi_manager_register_callback(wifi_event_handler);
// TEMPORARY: Clear stored credentials to force use of new ones
// wifi_manager_clear_credentials();
// ESP_LOGI(TAG, "Cleared stored WiFi credentials");
// Initialize OTA server
ESP_ERROR_CHECK(ota_server_init());
ota_server_set_version(APP_VERSION);
@ -180,6 +598,31 @@ void app_main(void)
mqtt_disconnected_callback,
mqtt_data_callback);
// Initialize Motor Control
ESP_ERROR_CHECK(motor_control_init());
motor_register_state_callback(motor_state_change_callback);
motor_register_error_callback(motor_error_callback);
// Configure motor safety limits
#ifdef MOTOR_TEST_MODE
// Use shorter limits for testing
ESP_LOGI(TAG, "MOTOR TEST MODE - Using short intervals for testing");
motor_set_max_runtime(MOTOR_PUMP_1, 30000); // 30 seconds max
motor_set_max_runtime(MOTOR_PUMP_2, 30000);
motor_set_min_interval(MOTOR_PUMP_1, 5000); // 5 seconds for testing
motor_set_min_interval(MOTOR_PUMP_2, 5000);
#else
// Use production values from Kconfig
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_min_interval(MOTOR_PUMP_1, CONFIG_WATERING_MIN_INTERVAL_MS);
motor_set_min_interval(MOTOR_PUMP_2, CONFIG_WATERING_MIN_INTERVAL_MS);
#endif
// Initialize Scheduler
ESP_ERROR_CHECK(scheduler_init());
scheduler_register_trigger_callback(scheduler_trigger_callback);
// Start WiFi connection
esp_err_t ret = wifi_manager_start();
if (ret != ESP_OK) {
@ -189,18 +632,51 @@ void app_main(void)
// Create sensor simulation task
xTaskCreate(sensor_simulation_task, "sensor_sim", 4096, NULL, 5, NULL);
// Create schedule status task
xTaskCreate(schedule_status_task, "schedule_status", 4096, NULL, 4, NULL);
// Main loop - monitor system status
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",
mqtt_client_is_connected() ? "Connected" : "Disconnected",
scheduler_is_time_synchronized() ? "Synced" : "Not synced",
esp_get_free_heap_size());
// Print pump states
// Print pump states and runtime
if (mqtt_client_is_connected()) {
ESP_LOGI(TAG, "Pump States - Pump1: %s, Pump2: %s",
test_pump_1 ? "ON" : "OFF",
test_pump_2 ? "ON" : "OFF");
for (int i = 1; i <= 2; i++) {
motor_stats_t stats;
motor_get_stats(i, &stats);
const char *state_str = "OFF";
if (motor_is_running(i)) {
state_str = "ON";
} else if (motor_is_cooldown(i)) {
state_str = "COOLDOWN";
}
ESP_LOGI(TAG, "Pump %d: %s, Total runtime: %lu s, Runs: %lu",
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

739
main/motor_control.c Normal file
View File

@ -0,0 +1,739 @@
#include <string.h>
#include <sys/time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "motor_control.h"
static const char *TAG = "MOTOR_CONTROL";
// Motor control structure
typedef struct {
motor_state_t state;
motor_dir_t direction;
uint8_t speed_percent;
uint8_t target_speed;
uint32_t max_runtime_ms;
uint32_t min_interval_ms;
uint8_t min_speed_percent;
uint8_t max_speed_percent;
// Runtime tracking
int64_t start_time;
int64_t last_stop_time;
TimerHandle_t safety_timer;
TimerHandle_t soft_start_timer;
// Statistics
motor_stats_t stats;
// GPIO pins
gpio_num_t in1_gpio;
gpio_num_t in2_gpio;
ledc_channel_t pwm_channel;
} motor_t;
// Global state
static motor_t s_motors[MOTOR_PUMP_MAX];
static SemaphoreHandle_t s_motor_mutex = NULL;
static bool s_initialized = false;
// Callbacks
static motor_state_callback_t s_state_callback = NULL;
static motor_error_callback_t s_error_callback = NULL;
// NVS namespace
#define MOTOR_NVS_NAMESPACE "motor_stats"
// Forward declarations
static esp_err_t motor_set_direction(motor_id_t id, motor_dir_t dir);
static esp_err_t motor_update_pwm(motor_id_t id, uint8_t duty);
static void motor_safety_timer_callback(TimerHandle_t xTimer);
static void motor_soft_start_timer_callback(TimerHandle_t xTimer);
static esp_err_t motor_save_stats(motor_id_t id);
static esp_err_t motor_load_stats(motor_id_t id);
static void motor_update_state(motor_id_t id, motor_state_t new_state);
// Utility functions
static int64_t get_time_ms(void)
{
return esp_timer_get_time() / 1000;
}
static bool is_valid_motor_id(motor_id_t id)
{
return (id == MOTOR_PUMP_1 || id == MOTOR_PUMP_2);
}
esp_err_t motor_control_init(void)
{
if (s_initialized) {
ESP_LOGW(TAG, "Motor control already initialized");
return ESP_OK;
}
esp_err_t ret = ESP_OK;
// Create mutex
s_motor_mutex = xSemaphoreCreateMutex();
if (s_motor_mutex == NULL) {
ESP_LOGE(TAG, "Failed to create mutex");
return ESP_ERR_NO_MEM;
}
// Configure standby pin (active low, so HIGH = enabled)
gpio_config_t io_conf = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = (1ULL << MOTOR_STBY_GPIO),
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.pull_up_en = GPIO_PULLUP_DISABLE,
};
ret = gpio_config(&io_conf);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure STBY pin");
goto error;
}
// Disable motors initially
gpio_set_level(MOTOR_STBY_GPIO, 0);
// Configure direction pins for both motors
io_conf.pin_bit_mask = (1ULL << MOTOR_AIN1_GPIO) | (1ULL << MOTOR_AIN2_GPIO) |
(1ULL << MOTOR_BIN1_GPIO) | (1ULL << MOTOR_BIN2_GPIO);
ret = gpio_config(&io_conf);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure direction pins");
goto error;
}
// Configure LEDC for PWM
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = MOTOR_PWM_RESOLUTION,
.freq_hz = MOTOR_PWM_FREQ_HZ,
.clk_cfg = LEDC_AUTO_CLK
};
ret = ledc_timer_config(&ledc_timer);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure LEDC timer");
goto error;
}
// Configure PWM channels
ledc_channel_config_t ledc_channel[2] = {
{
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.gpio_num = MOTOR_PWMA_GPIO,
.duty = 0,
.hpoint = 0
},
{
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_1,
.timer_sel = LEDC_TIMER_0,
.gpio_num = MOTOR_PWMB_GPIO,
.duty = 0,
.hpoint = 0
}
};
for (int i = 0; i < 2; i++) {
ret = ledc_channel_config(&ledc_channel[i]);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure LEDC channel %d", i);
goto error;
}
}
// Initialize motor structures
memset(s_motors, 0, sizeof(s_motors));
// Motor 1 (Pump 1)
s_motors[0].in1_gpio = MOTOR_AIN1_GPIO;
s_motors[0].in2_gpio = MOTOR_AIN2_GPIO;
s_motors[0].pwm_channel = LEDC_CHANNEL_0;
s_motors[0].max_runtime_ms = MOTOR_MAX_RUNTIME_MS;
s_motors[0].min_interval_ms = MOTOR_MIN_INTERVAL_MS;
s_motors[0].min_speed_percent = MOTOR_MIN_SPEED;
s_motors[0].max_speed_percent = 100;
s_motors[0].state = MOTOR_STATE_STOPPED;
s_motors[0].direction = MOTOR_DIR_FORWARD;
// Motor 2 (Pump 2)
s_motors[1].in1_gpio = MOTOR_BIN1_GPIO;
s_motors[1].in2_gpio = MOTOR_BIN2_GPIO;
s_motors[1].pwm_channel = LEDC_CHANNEL_1;
s_motors[1].max_runtime_ms = MOTOR_MAX_RUNTIME_MS;
s_motors[1].min_interval_ms = MOTOR_MIN_INTERVAL_MS;
s_motors[1].min_speed_percent = MOTOR_MIN_SPEED;
s_motors[1].max_speed_percent = 100;
s_motors[1].state = MOTOR_STATE_STOPPED;
s_motors[1].direction = MOTOR_DIR_FORWARD;
// Create safety timers
for (int i = 0; i < MOTOR_PUMP_MAX - 1; i++) {
char timer_name[32];
snprintf(timer_name, sizeof(timer_name), "motor_safety_%d", i + 1);
s_motors[i].safety_timer = xTimerCreate(timer_name,
pdMS_TO_TICKS(1000),
pdFALSE,
(void*)(i + 1),
motor_safety_timer_callback);
if (s_motors[i].safety_timer == NULL) {
ESP_LOGE(TAG, "Failed to create safety timer for motor %d", i + 1);
ret = ESP_ERR_NO_MEM;
goto error;
}
snprintf(timer_name, sizeof(timer_name), "motor_soft_%d", i + 1);
s_motors[i].soft_start_timer = xTimerCreate(timer_name,
pdMS_TO_TICKS(50),
pdTRUE,
(void*)(i + 1),
motor_soft_start_timer_callback);
if (s_motors[i].soft_start_timer == NULL) {
ESP_LOGE(TAG, "Failed to create soft start timer for motor %d", i + 1);
ret = ESP_ERR_NO_MEM;
goto error;
}
}
// Load statistics from NVS
for (int i = 0; i < MOTOR_PUMP_MAX - 1; i++) {
motor_load_stats(i + 1);
}
// Enable motor driver
gpio_set_level(MOTOR_STBY_GPIO, 1);
s_initialized = true;
ESP_LOGI(TAG, "Motor control initialized successfully");
return ESP_OK;
error:
if (s_motor_mutex) {
vSemaphoreDelete(s_motor_mutex);
s_motor_mutex = NULL;
}
return ret;
}
esp_err_t motor_control_deinit(void)
{
if (!s_initialized) {
return ESP_OK;
}
// Stop all motors
motor_stop_all();
// Disable motor driver
gpio_set_level(MOTOR_STBY_GPIO, 0);
// Delete timers
for (int i = 0; i < MOTOR_PUMP_MAX - 1; i++) {
if (s_motors[i].safety_timer) {
xTimerDelete(s_motors[i].safety_timer, 0);
}
if (s_motors[i].soft_start_timer) {
xTimerDelete(s_motors[i].soft_start_timer, 0);
}
}
// Delete mutex
if (s_motor_mutex) {
vSemaphoreDelete(s_motor_mutex);
s_motor_mutex = NULL;
}
s_initialized = false;
ESP_LOGI(TAG, "Motor control deinitialized");
return ESP_OK;
}
static esp_err_t motor_set_direction(motor_id_t id, motor_dir_t dir)
{
if (!is_valid_motor_id(id)) {
return ESP_ERR_INVALID_ARG;
}
motor_t *motor = &s_motors[id - 1];
if (dir == MOTOR_DIR_FORWARD) {
gpio_set_level(motor->in1_gpio, 1);
gpio_set_level(motor->in2_gpio, 0);
} else {
gpio_set_level(motor->in1_gpio, 0);
gpio_set_level(motor->in2_gpio, 1);
}
motor->direction = dir;
return ESP_OK;
}
static esp_err_t motor_update_pwm(motor_id_t id, uint8_t duty)
{
if (!is_valid_motor_id(id)) {
return ESP_ERR_INVALID_ARG;
}
motor_t *motor = &s_motors[id - 1];
esp_err_t ret = ledc_set_duty(LEDC_LOW_SPEED_MODE, motor->pwm_channel, duty);
if (ret != ESP_OK) {
return ret;
}
return ledc_update_duty(LEDC_LOW_SPEED_MODE, motor->pwm_channel);
}
static void motor_update_state(motor_id_t id, motor_state_t new_state)
{
motor_t *motor = &s_motors[id - 1];
motor_state_t old_state = motor->state;
motor->state = new_state;
if (old_state != new_state && s_state_callback) {
s_state_callback(id, new_state);
}
}
esp_err_t motor_start(motor_id_t id, uint8_t speed_percent)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (!is_valid_motor_id(id)) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_motor_mutex, portMAX_DELAY);
motor_t *motor = &s_motors[id - 1];
// Check if already running
if (motor->state == MOTOR_STATE_RUNNING) {
xSemaphoreGive(s_motor_mutex);
ESP_LOGW(TAG, "Motor %d already running", id);
return ESP_OK;
}
// Check cooldown period
int64_t now = get_time_ms();
if (motor->last_stop_time > 0) {
int64_t elapsed = now - motor->last_stop_time;
if (elapsed < motor->min_interval_ms) {
motor_update_state(id, MOTOR_STATE_COOLDOWN);
xSemaphoreGive(s_motor_mutex);
if (s_error_callback) {
s_error_callback(id, "Cooldown period not elapsed");
}
ESP_LOGW(TAG, "Motor %d in cooldown, %lld ms remaining",
id, motor->min_interval_ms - elapsed);
return ESP_ERR_INVALID_STATE;
}
}
// Clamp speed to configured limits
if (speed_percent < motor->min_speed_percent) {
speed_percent = motor->min_speed_percent;
}
if (speed_percent > motor->max_speed_percent) {
speed_percent = motor->max_speed_percent;
}
// Set direction
motor_set_direction(id, motor->direction);
// Store target speed for soft start
motor->target_speed = speed_percent;
motor->speed_percent = 0;
// Start with 0 PWM for soft start
motor_update_pwm(id, 0);
// Record start time
motor->start_time = now;
motor_update_state(id, MOTOR_STATE_RUNNING);
// Start soft start timer
xTimerStart(motor->soft_start_timer, 0);
// Start safety timer
xTimerChangePeriod(motor->safety_timer, pdMS_TO_TICKS(motor->max_runtime_ms), 0);
xTimerStart(motor->safety_timer, 0);
xSemaphoreGive(s_motor_mutex);
ESP_LOGI(TAG, "Motor %d started at %d%% speed", id, speed_percent);
return ESP_OK;
}
esp_err_t motor_stop(motor_id_t id)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (!is_valid_motor_id(id)) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_motor_mutex, portMAX_DELAY);
motor_t *motor = &s_motors[id - 1];
// Stop timers
xTimerStop(motor->safety_timer, 0);
xTimerStop(motor->soft_start_timer, 0);
// Set PWM to 0
motor_update_pwm(id, 0);
motor->speed_percent = 0;
// Update runtime statistics
if (motor->state == MOTOR_STATE_RUNNING) {
int64_t runtime = get_time_ms() - motor->start_time;
motor->stats.last_run_duration_ms = runtime;
motor->stats.total_runtime_ms += runtime;
motor->stats.last_run_timestamp = motor->start_time;
motor->stats.run_count++;
// Save stats to NVS periodically (every 10 runs)
if (motor->stats.run_count % 10 == 0) {
motor_save_stats(id);
}
}
motor->last_stop_time = get_time_ms();
motor_update_state(id, MOTOR_STATE_STOPPED);
xSemaphoreGive(s_motor_mutex);
ESP_LOGI(TAG, "Motor %d stopped", id);
return ESP_OK;
}
esp_err_t motor_stop_all(void)
{
esp_err_t ret = ESP_OK;
for (motor_id_t id = MOTOR_PUMP_1; id < MOTOR_PUMP_MAX; id++) {
esp_err_t err = motor_stop(id);
if (err != ESP_OK) {
ret = err;
}
}
return ret;
}
esp_err_t motor_emergency_stop(void)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
// Disable motor driver immediately
gpio_set_level(MOTOR_STBY_GPIO, 0);
// Stop all motors
motor_stop_all();
// Re-enable motor driver
gpio_set_level(MOTOR_STBY_GPIO, 1);
ESP_LOGW(TAG, "Emergency stop executed");
return ESP_OK;
}
esp_err_t motor_start_timed(motor_id_t id, uint8_t speed_percent, uint32_t duration_ms)
{
if (duration_ms > MOTOR_MAX_RUNTIME_MS) {
duration_ms = MOTOR_MAX_RUNTIME_MS;
}
esp_err_t ret = motor_start(id, speed_percent);
if (ret != ESP_OK) {
return ret;
}
// Override the safety timer with the requested duration
xSemaphoreTake(s_motor_mutex, portMAX_DELAY);
xTimerChangePeriod(s_motors[id - 1].safety_timer, pdMS_TO_TICKS(duration_ms), 0);
xSemaphoreGive(s_motor_mutex);
return ESP_OK;
}
esp_err_t motor_set_speed(motor_id_t id, uint8_t speed_percent)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (!is_valid_motor_id(id)) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_motor_mutex, portMAX_DELAY);
motor_t *motor = &s_motors[id - 1];
if (motor->state != MOTOR_STATE_RUNNING) {
xSemaphoreGive(s_motor_mutex);
return ESP_ERR_INVALID_STATE;
}
// Clamp speed
if (speed_percent < motor->min_speed_percent) {
speed_percent = motor->min_speed_percent;
}
if (speed_percent > motor->max_speed_percent) {
speed_percent = motor->max_speed_percent;
}
motor->speed_percent = speed_percent;
motor->target_speed = speed_percent;
uint8_t duty = (speed_percent * MOTOR_PWM_MAX_DUTY) / 100;
motor_update_pwm(id, duty);
xSemaphoreGive(s_motor_mutex);
return ESP_OK;
}
motor_state_t motor_get_state(motor_id_t id)
{
if (!is_valid_motor_id(id)) {
return MOTOR_STATE_ERROR;
}
return s_motors[id - 1].state;
}
bool motor_is_running(motor_id_t id)
{
return motor_get_state(id) == MOTOR_STATE_RUNNING;
}
bool motor_is_cooldown(motor_id_t id)
{
if (!is_valid_motor_id(id)) {
return false;
}
motor_t *motor = &s_motors[id - 1];
if (motor->last_stop_time == 0) {
return false;
}
int64_t elapsed = get_time_ms() - motor->last_stop_time;
return elapsed < motor->min_interval_ms;
}
uint32_t motor_get_runtime_ms(motor_id_t id)
{
if (!is_valid_motor_id(id)) {
return 0;
}
motor_t *motor = &s_motors[id - 1];
if (motor->state == MOTOR_STATE_RUNNING) {
return get_time_ms() - motor->start_time;
}
return 0;
}
esp_err_t motor_get_stats(motor_id_t id, motor_stats_t *stats)
{
if (!is_valid_motor_id(id) || stats == NULL) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_motor_mutex, portMAX_DELAY);
memcpy(stats, &s_motors[id - 1].stats, sizeof(motor_stats_t));
xSemaphoreGive(s_motor_mutex);
return ESP_OK;
}
esp_err_t motor_set_max_runtime(motor_id_t id, uint32_t max_runtime_ms)
{
if (!is_valid_motor_id(id)) {
return ESP_ERR_INVALID_ARG;
}
s_motors[id - 1].max_runtime_ms = max_runtime_ms;
return ESP_OK;
}
esp_err_t motor_set_min_interval(motor_id_t id, uint32_t min_interval_ms)
{
if (!is_valid_motor_id(id)) {
return ESP_ERR_INVALID_ARG;
}
s_motors[id - 1].min_interval_ms = min_interval_ms;
return ESP_OK;
}
esp_err_t motor_set_speed_limits(motor_id_t id, uint8_t min_speed, uint8_t max_speed)
{
if (!is_valid_motor_id(id)) {
return ESP_ERR_INVALID_ARG;
}
if (min_speed > max_speed || max_speed > 100) {
return ESP_ERR_INVALID_ARG;
}
s_motors[id - 1].min_speed_percent = min_speed;
s_motors[id - 1].max_speed_percent = max_speed;
return ESP_OK;
}
void motor_register_state_callback(motor_state_callback_t callback)
{
s_state_callback = callback;
}
void motor_register_error_callback(motor_error_callback_t callback)
{
s_error_callback = callback;
}
esp_err_t motor_test_run(motor_id_t id, uint32_t duration_ms)
{
ESP_LOGI(TAG, "Starting test run for motor %d, duration: %lu ms", id, duration_ms);
return motor_start_timed(id, MOTOR_DEFAULT_SPEED, duration_ms);
}
// Timer callbacks
static void motor_safety_timer_callback(TimerHandle_t xTimer)
{
motor_id_t id = (motor_id_t)(intptr_t)pvTimerGetTimerID(xTimer);
motor_t *motor = &s_motors[id - 1];
ESP_LOGW(TAG, "Safety timer expired for motor %d", id);
// Do minimal work in timer callback to avoid stack overflow
// Just stop the PWM and update state
ledc_set_duty(LEDC_LOW_SPEED_MODE, motor->pwm_channel, 0);
ledc_update_duty(LEDC_LOW_SPEED_MODE, motor->pwm_channel);
// Update basic state
motor->speed_percent = 0;
motor->state = MOTOR_STATE_STOPPED;
// Stats update can be deferred or done in main context
// For now, just record the stop time
motor->last_stop_time = get_time_ms();
}
static void motor_soft_start_timer_callback(TimerHandle_t xTimer)
{
motor_id_t id = (motor_id_t)(intptr_t)pvTimerGetTimerID(xTimer);
motor_t *motor = &s_motors[id - 1];
if (motor->state != MOTOR_STATE_RUNNING) {
xTimerStop(xTimer, 0);
return;
}
// Calculate increment based on soft start time
// We want to go from 0 to target in MOTOR_SOFT_START_TIME_MS
// Timer runs every 50ms, so number of steps = MOTOR_SOFT_START_TIME_MS / 50
int steps = MOTOR_SOFT_START_TIME_MS / 50;
int increment = motor->target_speed / steps;
if (increment < 1) increment = 1;
// Ramp up speed
if (motor->speed_percent < motor->target_speed) {
motor->speed_percent += increment;
if (motor->speed_percent > motor->target_speed) {
motor->speed_percent = motor->target_speed;
}
uint8_t duty = (motor->speed_percent * MOTOR_PWM_MAX_DUTY) / 100;
// Update PWM directly without taking mutex (atomic operation)
ledc_set_duty(LEDC_LOW_SPEED_MODE, motor->pwm_channel, duty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, motor->pwm_channel);
ESP_LOGD(TAG, "Soft start motor %d: %d%% (target: %d%%)",
id, motor->speed_percent, motor->target_speed);
// Stop timer when target reached
if (motor->speed_percent >= motor->target_speed) {
xTimerStop(xTimer, 0);
ESP_LOGD(TAG, "Soft start complete for motor %d", id);
}
}
}
// NVS functions
static esp_err_t motor_save_stats(motor_id_t id)
{
nvs_handle_t nvs_handle;
esp_err_t ret;
char key[16];
ret = nvs_open(MOTOR_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
if (ret != ESP_OK) {
return ret;
}
snprintf(key, sizeof(key), "motor%d_stats", id);
ret = nvs_set_blob(nvs_handle, key, &s_motors[id - 1].stats, sizeof(motor_stats_t));
if (ret == ESP_OK) {
nvs_commit(nvs_handle);
}
nvs_close(nvs_handle);
return ret;
}
static esp_err_t motor_load_stats(motor_id_t id)
{
nvs_handle_t nvs_handle;
esp_err_t ret;
char key[16];
size_t length = sizeof(motor_stats_t);
ret = nvs_open(MOTOR_NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
if (ret != ESP_OK) {
return ret;
}
snprintf(key, sizeof(key), "motor%d_stats", id);
ret = nvs_get_blob(nvs_handle, key, &s_motors[id - 1].stats, &length);
nvs_close(nvs_handle);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Loaded stats for motor %d: total runtime %lu ms, %lu runs",
id, s_motors[id - 1].stats.total_runtime_ms, s_motors[id - 1].stats.run_count);
}
return ret;
}

104
main/motor_control.h Normal file
View File

@ -0,0 +1,104 @@
#ifndef MOTOR_CONTROL_H
#define MOTOR_CONTROL_H
#include <stdbool.h>
#include "esp_err.h"
// TB6612FNG GPIO assignments
#define MOTOR_AIN1_GPIO 4 // Pump 1 Direction
#define MOTOR_AIN2_GPIO 5 // Pump 1 Direction
#define MOTOR_BIN1_GPIO 6 // Pump 2 Direction
#define MOTOR_BIN2_GPIO 7 // Pump 2 Direction
#define MOTOR_PWMA_GPIO 8 // Pump 1 Speed (PWM)
#define MOTOR_PWMB_GPIO 9 // Pump 2 Speed (PWM)
#define MOTOR_STBY_GPIO 10 // Standby (Active Low)
// PWM Configuration
#define MOTOR_PWM_FREQ_HZ 5000 // 5kHz PWM frequency
#define MOTOR_PWM_RESOLUTION 8 // 8-bit resolution (0-255)
#define MOTOR_PWM_MAX_DUTY 255 // Maximum duty cycle
// Safety Configuration
#define MOTOR_DEFAULT_SPEED 80 // Default pump speed (%)
#define MOTOR_MIN_SPEED 20 // Minimum pump speed (%)
// Default safety limits (can be overridden at runtime)
#define MOTOR_MAX_RUNTIME_MS 30000 // Default maximum runtime (30 seconds)
#define MOTOR_MIN_INTERVAL_MS 300000 // Default minimum interval between runs (5 minutes)
// Test mode limits (shorter for testing)
#define MOTOR_TEST_MAX_RUNTIME_MS 30000 // Test mode max runtime (30 seconds)
#define MOTOR_TEST_MIN_INTERVAL_MS 5000 // Test mode min interval (5 seconds)
#define MOTOR_SOFT_START_TIME_MS 500 // Soft start ramp time
// Motor IDs
typedef enum {
MOTOR_PUMP_1 = 1,
MOTOR_PUMP_2 = 2,
MOTOR_PUMP_MAX
} motor_id_t;
// Motor states
typedef enum {
MOTOR_STATE_STOPPED = 0,
MOTOR_STATE_RUNNING,
MOTOR_STATE_ERROR,
MOTOR_STATE_COOLDOWN
} motor_state_t;
// Motor direction (pumps are unidirectional, but driver supports both)
typedef enum {
MOTOR_DIR_FORWARD = 0,
MOTOR_DIR_REVERSE
} motor_dir_t;
// Motor runtime statistics
typedef struct {
uint32_t total_runtime_ms; // Total runtime in milliseconds
uint32_t last_run_duration_ms; // Last run duration
int64_t last_run_timestamp; // Timestamp of last run
uint32_t run_count; // Total number of runs
uint32_t error_count; // Total number of errors
} motor_stats_t;
// Callbacks
typedef void (*motor_state_callback_t)(motor_id_t id, motor_state_t state);
typedef void (*motor_error_callback_t)(motor_id_t id, const char* error);
// Motor control functions
esp_err_t motor_control_init(void);
esp_err_t motor_control_deinit(void);
// Basic control
esp_err_t motor_start(motor_id_t id, uint8_t speed_percent);
esp_err_t motor_stop(motor_id_t id);
esp_err_t motor_stop_all(void);
esp_err_t motor_emergency_stop(void);
// Advanced control
esp_err_t motor_start_timed(motor_id_t id, uint8_t speed_percent, uint32_t duration_ms);
esp_err_t motor_pulse(motor_id_t id, uint8_t speed_percent, uint32_t on_time_ms, uint32_t off_time_ms, uint32_t cycles);
esp_err_t motor_set_speed(motor_id_t id, uint8_t speed_percent);
// Status and configuration
motor_state_t motor_get_state(motor_id_t id);
bool motor_is_running(motor_id_t id);
bool motor_is_cooldown(motor_id_t id);
uint32_t motor_get_runtime_ms(motor_id_t id);
esp_err_t motor_get_stats(motor_id_t id, motor_stats_t *stats);
// Safety configuration
esp_err_t motor_set_max_runtime(motor_id_t id, uint32_t max_runtime_ms);
esp_err_t motor_set_min_interval(motor_id_t id, uint32_t min_interval_ms);
esp_err_t motor_set_speed_limits(motor_id_t id, uint8_t min_speed, uint8_t max_speed);
// Callbacks
void motor_register_state_callback(motor_state_callback_t callback);
void motor_register_error_callback(motor_error_callback_t callback);
// Calibration and testing
esp_err_t motor_test_run(motor_id_t id, uint32_t duration_ms);
esp_err_t motor_calibrate_flow(motor_id_t id);
#endif // MOTOR_CONTROL_H

242
main/motor_test.c Normal file
View File

@ -0,0 +1,242 @@
/**
* Motor Control Hardware Test Program
*
* This is a standalone test program to verify TB6612FNG motor driver
* connections before integrating with the full system.
*
* Test sequence:
* 1. Initialize motor control
* 2. Test each pump individually
* 3. Test PWM speed control
* 4. Test safety features
* 5. Test both pumps together
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "motor_control.h"
static const char *TAG = "MOTOR_TEST";
// Test callbacks
static void test_state_callback(motor_id_t id, motor_state_t state)
{
const char *state_str[] = {"STOPPED", "RUNNING", "ERROR", "COOLDOWN"};
ESP_LOGI(TAG, "Motor %d state: %s", id, state_str[state]);
}
static void test_error_callback(motor_id_t id, const char* error)
{
ESP_LOGE(TAG, "Motor %d error: %s", id, error);
}
void app_main(void)
{
ESP_LOGI(TAG, "=== TB6612FNG Motor Driver Test Program ===");
ESP_LOGI(TAG, "GPIO Connections:");
ESP_LOGI(TAG, " AIN1 (Pump1 Dir): GPIO%d", MOTOR_AIN1_GPIO);
ESP_LOGI(TAG, " AIN2 (Pump1 Dir): GPIO%d", MOTOR_AIN2_GPIO);
ESP_LOGI(TAG, " BIN1 (Pump2 Dir): GPIO%d", MOTOR_BIN1_GPIO);
ESP_LOGI(TAG, " BIN2 (Pump2 Dir): GPIO%d", MOTOR_BIN2_GPIO);
ESP_LOGI(TAG, " PWMA (Pump1 PWM): GPIO%d", MOTOR_PWMA_GPIO);
ESP_LOGI(TAG, " PWMB (Pump2 PWM): GPIO%d", MOTOR_PWMB_GPIO);
ESP_LOGI(TAG, " STBY (Standby): GPIO%d", MOTOR_STBY_GPIO);
// Initialize motor control
ESP_LOGI(TAG, "\n--- Initializing Motor Control ---");
esp_err_t ret = motor_control_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize motor control: %s", esp_err_to_name(ret));
return;
}
// Register callbacks
motor_register_state_callback(test_state_callback);
motor_register_error_callback(test_error_callback);
// Wait a bit
vTaskDelay(pdMS_TO_TICKS(2000));
// Test 1: Basic ON/OFF for Pump 1
ESP_LOGI(TAG, "\n--- Test 1: Pump 1 Basic ON/OFF ---");
ESP_LOGI(TAG, "Starting Pump 1 at default speed (%d%%)", MOTOR_DEFAULT_SPEED);
motor_start(MOTOR_PUMP_1, MOTOR_DEFAULT_SPEED);
vTaskDelay(pdMS_TO_TICKS(3000));
ESP_LOGI(TAG, "Stopping Pump 1");
motor_stop(MOTOR_PUMP_1);
vTaskDelay(pdMS_TO_TICKS(2000));
// Test 2: Basic ON/OFF for Pump 2
ESP_LOGI(TAG, "\n--- Test 2: Pump 2 Basic ON/OFF ---");
ESP_LOGI(TAG, "Starting Pump 2 at default speed (%d%%)", MOTOR_DEFAULT_SPEED);
motor_start(MOTOR_PUMP_2, MOTOR_DEFAULT_SPEED);
vTaskDelay(pdMS_TO_TICKS(3000));
ESP_LOGI(TAG, "Stopping Pump 2");
motor_stop(MOTOR_PUMP_2);
vTaskDelay(pdMS_TO_TICKS(2000));
// Test 3: PWM Speed Control
ESP_LOGI(TAG, "\n--- Test 3: PWM Speed Control (Pump 1) ---");
int speeds[] = {30, 50, 70, 90, 100};
for (int i = 0; i < sizeof(speeds)/sizeof(speeds[0]); i++) {
ESP_LOGI(TAG, "Testing speed: %d%%", speeds[i]);
motor_start(MOTOR_PUMP_1, speeds[i]);
vTaskDelay(pdMS_TO_TICKS(2000));
}
motor_stop(MOTOR_PUMP_1);
vTaskDelay(pdMS_TO_TICKS(2000));
// Test 4: Timed Run
ESP_LOGI(TAG, "\n--- Test 4: Timed Run (5 seconds) ---");
ESP_LOGI(TAG, "Starting Pump 1 for 5 seconds");
motor_start_timed(MOTOR_PUMP_1, 70, 5000);
vTaskDelay(pdMS_TO_TICKS(7000)); // Wait for completion
// Test 5: Both pumps together
ESP_LOGI(TAG, "\n--- Test 5: Both Pumps Together ---");
ESP_LOGI(TAG, "Starting both pumps");
motor_start(MOTOR_PUMP_1, 60);
motor_start(MOTOR_PUMP_2, 80);
vTaskDelay(pdMS_TO_TICKS(3000));
ESP_LOGI(TAG, "Stopping both pumps");
motor_stop_all();
vTaskDelay(pdMS_TO_TICKS(2000));
// Test 6: Safety - Cooldown Period
ESP_LOGI(TAG, "\n--- Test 6: Cooldown Period Test ---");
motor_set_min_interval(MOTOR_PUMP_1, 5000); // 5 second cooldown for test
ESP_LOGI(TAG, "Starting pump 1");
motor_start(MOTOR_PUMP_1, 50);
vTaskDelay(pdMS_TO_TICKS(2000));
motor_stop(MOTOR_PUMP_1);
ESP_LOGI(TAG, "Attempting to restart immediately (should fail)");
ret = motor_start(MOTOR_PUMP_1, 50);
if (ret != ESP_OK) {
ESP_LOGI(TAG, "Good! Cooldown protection working");
}
ESP_LOGI(TAG, "Waiting for cooldown period...");
vTaskDelay(pdMS_TO_TICKS(6000));
ESP_LOGI(TAG, "Attempting to start after cooldown");
ret = motor_start(MOTOR_PUMP_1, 50);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Good! Pump started after cooldown");
vTaskDelay(pdMS_TO_TICKS(2000));
motor_stop(MOTOR_PUMP_1);
}
// Test 7: Speed change while running
ESP_LOGI(TAG, "\n--- Test 7: Speed Change While Running ---");
ESP_LOGI(TAG, "Starting at 30%%");
motor_start(MOTOR_PUMP_1, 30);
vTaskDelay(pdMS_TO_TICKS(2000));
ESP_LOGI(TAG, "Changing to 70%%");
motor_set_speed(MOTOR_PUMP_1, 70);
vTaskDelay(pdMS_TO_TICKS(2000));
ESP_LOGI(TAG, "Changing to 100%%");
motor_set_speed(MOTOR_PUMP_1, 100);
vTaskDelay(pdMS_TO_TICKS(2000));
motor_stop(MOTOR_PUMP_1);
// Test 8: Emergency Stop
ESP_LOGI(TAG, "\n--- Test 8: Emergency Stop ---");
ESP_LOGI(TAG, "Starting both pumps");
motor_start(MOTOR_PUMP_1, 80);
motor_start(MOTOR_PUMP_2, 80);
vTaskDelay(pdMS_TO_TICKS(2000));
ESP_LOGI(TAG, "Triggering emergency stop!");
motor_emergency_stop();
ESP_LOGI(TAG, "Checking states after emergency stop");
if (!motor_is_running(MOTOR_PUMP_1) && !motor_is_running(MOTOR_PUMP_2)) {
ESP_LOGI(TAG, "Good! Both pumps stopped");
}
vTaskDelay(pdMS_TO_TICKS(2000));
// Test 9: Get Statistics
ESP_LOGI(TAG, "\n--- Test 9: Runtime Statistics ---");
motor_stats_t stats;
for (int i = 1; i <= 2; i++) {
if (motor_get_stats(i, &stats) == ESP_OK) {
ESP_LOGI(TAG, "Pump %d Statistics:", i);
ESP_LOGI(TAG, " Total runtime: %lu ms", stats.total_runtime_ms);
ESP_LOGI(TAG, " Run count: %lu", stats.run_count);
ESP_LOGI(TAG, " Last duration: %lu ms", stats.last_run_duration_ms);
ESP_LOGI(TAG, " Error count: %lu", stats.error_count);
}
}
// Test 10: Maximum runtime safety
ESP_LOGI(TAG, "\n--- Test 10: Maximum Runtime Safety ---");
motor_set_max_runtime(MOTOR_PUMP_1, 3000); // 3 second max for test
ESP_LOGI(TAG, "Starting pump 1 (should auto-stop after 3 seconds)");
motor_start(MOTOR_PUMP_1, 50);
// Wait for safety timer
vTaskDelay(pdMS_TO_TICKS(5000));
if (!motor_is_running(MOTOR_PUMP_1)) {
ESP_LOGI(TAG, "Good! Safety timer stopped the pump");
}
// Test 11: Soft Start Observation
ESP_LOGI(TAG, "\n--- Test 11: Soft Start Observation ---");
ESP_LOGI(TAG, "Watch/listen for gradual speed increase over 500ms");
motor_start(MOTOR_PUMP_1, 100);
vTaskDelay(pdMS_TO_TICKS(3000));
motor_stop(MOTOR_PUMP_1);
// Final test summary
ESP_LOGI(TAG, "\n=== All Tests Complete ===");
ESP_LOGI(TAG, "Test Summary:");
ESP_LOGI(TAG, " ✓ Basic ON/OFF control");
ESP_LOGI(TAG, " ✓ PWM speed control");
ESP_LOGI(TAG, " ✓ Timed operations");
ESP_LOGI(TAG, " ✓ Dual pump control");
ESP_LOGI(TAG, " ✓ Safety features");
ESP_LOGI(TAG, " ✓ Emergency stop");
ESP_LOGI(TAG, " ✓ Statistics tracking");
ESP_LOGI(TAG, "");
ESP_LOGI(TAG, "Monitor the pumps to ensure they responded correctly to all commands");
ESP_LOGI(TAG, "Check for any unusual noises, heating, or behavior");
// Keep running and print status periodically
ESP_LOGI(TAG, "\nEntering monitoring mode - System status every 10 seconds");
while (1) {
vTaskDelay(pdMS_TO_TICKS(10000));
ESP_LOGI(TAG, "--- System Status ---");
ESP_LOGI(TAG, "Free heap: %d bytes", esp_get_free_heap_size());
for (int i = 1; i <= 2; i++) {
motor_stats_t current_stats;
if (motor_get_stats(i, &current_stats) == ESP_OK) {
const char *state = "IDLE";
if (motor_is_running(i)) {
state = "RUNNING";
} else if (motor_is_cooldown(i)) {
state = "COOLDOWN";
}
ESP_LOGI(TAG, "Pump %d: State=%s, Total runs=%lu, Total time=%lu s",
i, state, current_stats.run_count,
current_stats.total_runtime_ms / 1000);
}
}
}
}

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

99
mqtt_topic_plan.txt Normal file
View File

@ -0,0 +1,99 @@
plant_watering/
├── status/
│ ├── esp32/connected # ESP32 connection status (retained)
│ ├── esp32/ip # ESP32 IP address (retained)
│ ├── esp32/uptime # System uptime in seconds
│ ├── esp32/version # Firmware version (retained)
│ ├── esp32/rssi # WiFi signal strength
│ ├── esp32/free_heap # Free memory for diagnostics
│ └── esp32/restart_reason # Last restart reason (retained)
├── pump/1/
│ ├── command # Commands: ON/OFF/PULSE
│ ├── status # Current status (retained)
│ ├── runtime # Last run duration in seconds
│ ├── total_runtime # Total runtime counter in seconds
│ ├── last_activated # Timestamp of last activation
│ └── flow_rate # If flow sensor added later
├── pump/2/
│ ├── command # Commands: ON/OFF/PULSE
│ ├── status # Current status (retained)
│ ├── runtime # Last run duration in seconds
│ ├── total_runtime # Total runtime counter in seconds
│ ├── last_activated # Timestamp of last activation
│ └── flow_rate # If flow sensor added later
├── sensor/1/
│ ├── moisture # Current moisture reading (0-4095)
│ ├── moisture_percent # Moisture as percentage
│ ├── last_watered # Timestamp of last watering
│ ├── temperature # Soil temperature if sensor supports
│ └── calibration/
│ ├── dry_value # Calibration point for dry
│ └── wet_value # Calibration point for wet
├── sensor/2/
│ ├── moisture # Current moisture reading (0-4095)
│ ├── moisture_percent # Moisture as percentage
│ ├── last_watered # Timestamp of last watering
│ ├── temperature # Soil temperature if sensor supports
│ └── calibration/
│ ├── dry_value # Calibration point for dry
│ └── wet_value # Calibration point for wet
├── settings/
│ ├── pump/1/
│ │ ├── moisture_threshold # Trigger threshold (0-100%)
│ │ ├── water_duration # Watering duration in seconds
│ │ ├── min_interval # Minimum hours between watering
│ │ ├── max_duration # Safety maximum runtime
│ │ └── enabled # Enable/disable pump
│ ├── pump/2/
│ │ ├── moisture_threshold # Trigger threshold (0-100%)
│ │ ├── water_duration # Watering duration in seconds
│ │ ├── min_interval # Minimum hours between watering
│ │ ├── max_duration # Safety maximum runtime
│ │ └── enabled # Enable/disable pump
│ └── system/
│ ├── report_interval # How often to publish sensor data
│ ├── timezone # For scheduling features
│ └── auto_mode # Global auto-watering enable
├── alerts/
│ ├── low_moisture/1 # Zone 1 moisture too low
│ ├── low_moisture/2 # Zone 2 moisture too low
│ ├── pump_error/1 # Pump 1 malfunction
│ ├── pump_error/2 # Pump 2 malfunction
│ ├── sensor_error/1 # Sensor 1 reading issues
│ ├── sensor_error/2 # Sensor 2 reading issues
│ └── water_tank_low # If tank sensor added
└── commands/
├── calibrate/sensor/1 # Trigger calibration mode
├── calibrate/sensor/2 # Trigger calibration mode
├── restart # Restart ESP32
├── factory_reset # Clear all settings
└── ota/url # Trigger OTA from URL
Additional considerations:
Timestamps: Use ISO 8601 format (e.g., "2024-01-15T14:30:00Z") for consistency
Retained messages: Mark critical status messages as retained (as you've done)
QoS levels:
QoS 0 for frequent sensor readings
QoS 1 for commands and state changes
QoS 2 for critical alerts (if needed)
JSON payloads: Consider using JSON for complex data:
// plant_watering/status/esp32/info
{
"version": "2.0.0",
"uptime": 3600,
"free_heap": 45632,
"rssi": -65,
"ip": "192.168.1.42"
}
Home Assistant Discovery: Add discovery topics if planning HA integration:
homeassistant/sensor/plant_watering_moisture_1/config
homeassistant/switch/plant_watering_pump_1/config

217
pinout.svg Normal file
View File

@ -0,0 +1,217 @@
<svg viewBox="0 0 1000 700" xmlns="http://www.w3.org/2000/svg">
<!-- Title -->
<text x="500" y="30" text-anchor="middle" font-size="24" font-weight="bold">ESP32-S3-MINI-1 Plant Watering System</text>
<!-- ESP32-S3-MINI-1 -->
<g id="esp32">
<rect x="50" y="200" width="200" height="300" fill="#2C3E50" stroke="black" stroke-width="2"/>
<text x="150" y="190" text-anchor="middle" font-size="16" font-weight="bold">ESP32-S3-MINI-1</text>
<!-- Left pins -->
<text x="40" y="235" text-anchor="end" font-size="12">3V3</text>
<circle cx="60" cy="230" r="4" fill="red"/>
<text x="40" y="255" text-anchor="end" font-size="12">GND</text>
<circle cx="60" cy="250" r="4" fill="black"/>
<text x="40" y="295" text-anchor="end" font-size="12">GPIO4</text>
<circle cx="60" cy="290" r="4" fill="yellow"/>
<text x="65" y="295" font-size="10" fill="blue">(AIN1)</text>
<text x="40" y="315" text-anchor="end" font-size="12">GPIO5</text>
<circle cx="60" cy="310" r="4" fill="yellow"/>
<text x="65" y="315" font-size="10" fill="blue">(AIN2)</text>
<text x="40" y="335" text-anchor="end" font-size="12">GPIO6</text>
<circle cx="60" cy="330" r="4" fill="yellow"/>
<text x="65" y="335" font-size="10" fill="blue">(BIN1)</text>
<text x="40" y="355" text-anchor="end" font-size="12">GPIO7</text>
<circle cx="60" cy="350" r="4" fill="yellow"/>
<text x="65" y="355" font-size="10" fill="blue">(BIN2)</text>
<text x="40" y="375" text-anchor="end" font-size="12">GPIO8</text>
<circle cx="60" cy="370" r="4" fill="orange"/>
<text x="65" y="375" font-size="10" fill="blue">(PWMA)</text>
<text x="40" y="395" text-anchor="end" font-size="12">GPIO9</text>
<circle cx="60" cy="390" r="4" fill="orange"/>
<text x="65" y="395" font-size="10" fill="blue">(PWMB)</text>
<text x="40" y="415" text-anchor="end" font-size="12">GPIO10</text>
<circle cx="60" cy="410" r="4" fill="purple"/>
<text x="65" y="415" font-size="10" fill="blue">(STBY)</text>
<!-- Right pins -->
<text x="260" y="295" text-anchor="start" font-size="12">GPIO1</text>
<circle cx="240" cy="290" r="4" fill="green"/>
<text x="235" y="295" text-anchor="end" font-size="10" fill="blue">(ADC1)</text>
<text x="260" y="315" text-anchor="start" font-size="12">GPIO2</text>
<circle cx="240" cy="310" r="4" fill="green"/>
<text x="235" y="315" text-anchor="end" font-size="10" fill="blue">(ADC2)</text>
</g>
<!-- TB6612FNG Motor Driver -->
<g id="motor-driver">
<rect x="400" y="200" width="180" height="300" fill="#34495E" stroke="black" stroke-width="2"/>
<text x="490" y="190" text-anchor="middle" font-size="16" font-weight="bold">TB6612FNG</text>
<!-- Left side pins -->
<text x="390" y="225" text-anchor="end" font-size="12">VM</text>
<circle cx="410" cy="220" r="4" fill="red"/>
<text x="390" y="245" text-anchor="end" font-size="12">VCC</text>
<circle cx="410" cy="240" r="4" fill="red"/>
<text x="390" y="265" text-anchor="end" font-size="12">GND</text>
<circle cx="410" cy="260" r="4" fill="black"/>
<text x="390" y="285" text-anchor="end" font-size="12">AIN1</text>
<circle cx="410" cy="280" r="4" fill="yellow"/>
<text x="390" y="305" text-anchor="end" font-size="12">AIN2</text>
<circle cx="410" cy="300" r="4" fill="yellow"/>
<text x="390" y="325" text-anchor="end" font-size="12">BIN1</text>
<circle cx="410" cy="320" r="4" fill="yellow"/>
<text x="390" y="345" text-anchor="end" font-size="12">BIN2</text>
<circle cx="410" cy="340" r="4" fill="yellow"/>
<text x="390" y="365" text-anchor="end" font-size="12">PWMA</text>
<circle cx="410" cy="360" r="4" fill="orange"/>
<text x="390" y="385" text-anchor="end" font-size="12">PWMB</text>
<circle cx="410" cy="380" r="4" fill="orange"/>
<text x="390" y="405" text-anchor="end" font-size="12">STBY</text>
<circle cx="410" cy="400" r="4" fill="purple"/>
<!-- Right side pins -->
<text x="590" y="245" text-anchor="start" font-size="12">A01</text>
<circle cx="570" cy="240" r="4" fill="cyan"/>
<text x="590" y="265" text-anchor="start" font-size="12">A02</text>
<circle cx="570" cy="260" r="4" fill="cyan"/>
<text x="590" y="345" text-anchor="start" font-size="12">B01</text>
<circle cx="570" cy="340" r="4" fill="cyan"/>
<text x="590" y="365" text-anchor="start" font-size="12">B02</text>
<circle cx="570" cy="360" r="4" fill="cyan"/>
</g>
<!-- Pump 1 -->
<g id="pump1">
<rect x="700" y="210" width="80" height="80" fill="#3498DB" stroke="black" stroke-width="2"/>
<text x="740" y="245" text-anchor="middle" font-size="14" font-weight="bold">Pump 1</text>
<text x="740" y="265" text-anchor="middle" font-size="12">12V DC</text>
<circle cx="710" cy="250" r="4" fill="cyan"/>
<circle cx="710" cy="270" r="4" fill="cyan"/>
</g>
<!-- Pump 2 -->
<g id="pump2">
<rect x="700" y="320" width="80" height="80" fill="#3498DB" stroke="black" stroke-width="2"/>
<text x="740" y="355" text-anchor="middle" font-size="14" font-weight="bold">Pump 2</text>
<text x="740" y="375" text-anchor="middle" font-size="12">12V DC</text>
<circle cx="710" cy="360" r="4" fill="cyan"/>
<circle cx="710" cy="380" r="4" fill="cyan"/>
</g>
<!-- Soil Moisture Sensor 1 -->
<g id="moisture1">
<rect x="50" y="550" width="120" height="80" fill="#8B4513" stroke="black" stroke-width="2"/>
<text x="110" y="540" text-anchor="middle" font-size="14" font-weight="bold">Soil Sensor 1</text>
<text x="65" y="575" font-size="12">VCC</text>
<circle cx="160" cy="570" r="4" fill="red"/>
<text x="65" y="595" font-size="12">GND</text>
<circle cx="160" cy="590" r="4" fill="black"/>
<text x="65" y="615" font-size="12">SIG</text>
<circle cx="160" cy="610" r="4" fill="green"/>
</g>
<!-- Soil Moisture Sensor 2 -->
<g id="moisture2">
<rect x="250" y="550" width="120" height="80" fill="#8B4513" stroke="black" stroke-width="2"/>
<text x="310" y="540" text-anchor="middle" font-size="14" font-weight="bold">Soil Sensor 2</text>
<text x="265" y="575" font-size="12">VCC</text>
<circle cx="360" cy="570" r="4" fill="red"/>
<text x="265" y="595" font-size="12">GND</text>
<circle cx="360" cy="590" r="4" fill="black"/>
<text x="265" y="615" font-size="12">SIG</text>
<circle cx="360" cy="610" r="4" fill="green"/>
</g>
<!-- Power Supply -->
<g id="power">
<rect x="700" y="450" width="100" height="80" fill="#E74C3C" stroke="black" stroke-width="2"/>
<text x="750" y="440" text-anchor="middle" font-size="14" font-weight="bold">12V Power</text>
<text x="750" y="480" text-anchor="middle" font-size="12">12V DC</text>
<text x="750" y="500" text-anchor="middle" font-size="12">2A min</text>
<circle cx="710" cy="470" r="4" fill="red"/>
<circle cx="710" cy="490" r="4" fill="black"/>
</g>
<!-- Connections -->
<!-- ESP32 to Motor Driver -->
<line x1="60" y1="290" x2="410" y2="280" stroke="yellow" stroke-width="2"/>
<line x1="60" y1="310" x2="410" y2="300" stroke="yellow" stroke-width="2"/>
<line x1="60" y1="330" x2="410" y2="320" stroke="yellow" stroke-width="2"/>
<line x1="60" y1="350" x2="410" y2="340" stroke="yellow" stroke-width="2"/>
<line x1="60" y1="370" x2="410" y2="360" stroke="orange" stroke-width="2"/>
<line x1="60" y1="390" x2="410" y2="380" stroke="orange" stroke-width="2"/>
<line x1="60" y1="410" x2="410" y2="400" stroke="purple" stroke-width="2"/>
<!-- Power connections -->
<line x1="60" y1="230" x2="410" y2="240" stroke="red" stroke-width="2"/>
<line x1="60" y1="250" x2="410" y2="260" stroke="black" stroke-width="2"/>
<!-- Motor Driver to Pumps -->
<line x1="570" y1="240" x2="710" y2="250" stroke="cyan" stroke-width="2"/>
<line x1="570" y1="260" x2="710" y2="270" stroke="cyan" stroke-width="2"/>
<line x1="570" y1="340" x2="710" y2="360" stroke="cyan" stroke-width="2"/>
<line x1="570" y1="360" x2="710" y2="380" stroke="cyan" stroke-width="2"/>
<!-- Power to Motor Driver -->
<line x1="710" y1="470" x2="410" y2="220" stroke="red" stroke-width="2"/>
<line x1="710" y1="490" x2="410" y2="260" stroke="black" stroke-width="2"/>
<!-- ESP32 to Soil Sensors -->
<line x1="240" y1="290" x2="160" y2="610" stroke="green" stroke-width="2"/>
<line x1="240" y1="310" x2="360" y2="610" stroke="green" stroke-width="2"/>
<!-- Power to Soil Sensors -->
<path d="M 60 230 L 30 230 L 30 520 L 160 520 L 160 570" stroke="red" stroke-width="2" fill="none"/>
<line x1="160" y1="520" x2="360" y2="520" stroke="red" stroke-width="2"/>
<line x1="360" y1="520" x2="360" y2="570" stroke="red" stroke-width="2"/>
<path d="M 60 250 L 20 250 L 20 530 L 160 530 L 160 590" stroke="black" stroke-width="2" fill="none"/>
<line x1="160" y1="530" x2="360" y2="530" stroke="black" stroke-width="2"/>
<line x1="360" y1="530" x2="360" y2="590" stroke="black" stroke-width="2"/>
<!-- Legend -->
<g id="legend">
<rect x="820" y="200" width="160" height="200" fill="#ECF0F1" stroke="black" stroke-width="1"/>
<text x="900" y="220" text-anchor="middle" font-size="14" font-weight="bold">Wire Colors</text>
<line x1="830" y1="240" x2="860" y2="240" stroke="red" stroke-width="2"/>
<text x="870" y="245" font-size="12">Power (3.3V/12V)</text>
<line x1="830" y1="260" x2="860" y2="260" stroke="black" stroke-width="2"/>
<text x="870" y="265" font-size="12">Ground</text>
<line x1="830" y1="280" x2="860" y2="280" stroke="yellow" stroke-width="2"/>
<text x="870" y="285" font-size="12">Direction Control</text>
<line x1="830" y1="300" x2="860" y2="300" stroke="orange" stroke-width="2"/>
<text x="870" y="305" font-size="12">PWM Speed</text>
<line x1="830" y1="320" x2="860" y2="320" stroke="purple" stroke-width="2"/>
<text x="870" y="325" font-size="12">Standby</text>
<line x1="830" y1="340" x2="860" y2="340" stroke="green" stroke-width="2"/>
<text x="870" y="345" font-size="12">Analog Signal</text>
<line x1="830" y1="360" x2="860" y2="360" stroke="cyan" stroke-width="2"/>
<text x="870" y="365" font-size="12">Motor Output</text>
</g>
<!-- Notes -->
<text x="50" y="670" font-size="12" font-weight="bold">Notes:</text>
<text x="50" y="685" font-size="11">• VM (Motor Voltage): 12V DC for pumps | • VCC (Logic Voltage): 3.3V from ESP32 | • STBY must be HIGH to enable motors</text>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

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