Compare commits
8 Commits
5fd11369cc
...
scheduler
| Author | SHA1 | Date | |
|---|---|---|---|
| a46604d970 | |||
| 1cd35970c1 | |||
| d4fb078a40 | |||
| 9ca88df1cb | |||
| ba43d22a1a | |||
| 5a4c91fbd3 | |||
| fef8da2de2 | |||
| 18e4041514 |
267
MOTOR_CONTROL_README.md
Normal file
267
MOTOR_CONTROL_README.md
Normal 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
413
MQTT_README.md
Normal 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
422
SCHEDULER_README.md
Normal 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
8
cmd.txt
Normal 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
|
||||||
@ -5,6 +5,8 @@ idf_component_register(
|
|||||||
"ota_server.c"
|
"ota_server.c"
|
||||||
"plant_mqtt.c"
|
"plant_mqtt.c"
|
||||||
"led_strip.c"
|
"led_strip.c"
|
||||||
|
"motor_control.c"
|
||||||
|
"scheduler.c"
|
||||||
INCLUDE_DIRS
|
INCLUDE_DIRS
|
||||||
"."
|
"."
|
||||||
REQUIRES
|
REQUIRES
|
||||||
|
|||||||
536
main/main.c
536
main/main.c
@ -9,18 +9,97 @@
|
|||||||
#include "wifi_manager.h"
|
#include "wifi_manager.h"
|
||||||
#include "ota_server.h"
|
#include "ota_server.h"
|
||||||
#include "plant_mqtt.h"
|
#include "plant_mqtt.h"
|
||||||
|
#include "motor_control.h"
|
||||||
|
#include "scheduler.h"
|
||||||
#include "sdkconfig.h"
|
#include "sdkconfig.h"
|
||||||
|
|
||||||
|
// Uncomment this line to enable motor test mode with shorter intervals
|
||||||
|
// #define MOTOR_TEST_MODE
|
||||||
|
|
||||||
static const char *TAG = "MAIN";
|
static const char *TAG = "MAIN";
|
||||||
|
|
||||||
// Application version
|
// Application version
|
||||||
#define APP_VERSION "2.0.0-mqtt"
|
#define APP_VERSION "2.2.0-scheduler"
|
||||||
|
|
||||||
// Test data
|
// Test data
|
||||||
static int test_moisture_1 = 45;
|
static int test_moisture_1 = 45;
|
||||||
static int test_moisture_2 = 62;
|
static int test_moisture_2 = 62;
|
||||||
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
|
// MQTT Callbacks
|
||||||
static void mqtt_connected_callback(void)
|
static void mqtt_connected_callback(void)
|
||||||
@ -30,8 +109,49 @@ static void mqtt_connected_callback(void)
|
|||||||
// Publish initial states
|
// Publish initial states
|
||||||
mqtt_publish_moisture(1, test_moisture_1);
|
mqtt_publish_moisture(1, test_moisture_1);
|
||||||
mqtt_publish_moisture(2, test_moisture_2);
|
mqtt_publish_moisture(2, test_moisture_2);
|
||||||
mqtt_publish_pump_state(1, test_pump_1);
|
mqtt_publish_pump_state(1, motor_is_running(MOTOR_PUMP_1));
|
||||||
mqtt_publish_pump_state(2, test_pump_2);
|
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)
|
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
|
// Handle pump control commands
|
||||||
if (strcmp(topic, TOPIC_PUMP_1_CMD) == 0) {
|
if (strcmp(topic, TOPIC_PUMP_1_CMD) == 0) {
|
||||||
if (strncmp(data, "on", data_len) == 0) {
|
if (strncmp(data, "on", data_len) == 0) {
|
||||||
test_pump_1 = true;
|
ESP_LOGI(TAG, "Starting pump 1 via MQTT");
|
||||||
ESP_LOGI(TAG, "Pump 1 turned ON");
|
esp_err_t ret = motor_start(MOTOR_PUMP_1, MOTOR_DEFAULT_SPEED);
|
||||||
mqtt_publish_pump_state(1, test_pump_1);
|
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) {
|
} else if (strncmp(data, "off", data_len) == 0) {
|
||||||
test_pump_1 = false;
|
ESP_LOGI(TAG, "Stopping pump 1 via MQTT");
|
||||||
ESP_LOGI(TAG, "Pump 1 turned OFF");
|
motor_stop(MOTOR_PUMP_1);
|
||||||
mqtt_publish_pump_state(1, test_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) {
|
} else if (strcmp(topic, TOPIC_PUMP_2_CMD) == 0) {
|
||||||
if (strncmp(data, "on", data_len) == 0) {
|
if (strncmp(data, "on", data_len) == 0) {
|
||||||
test_pump_2 = true;
|
ESP_LOGI(TAG, "Starting pump 2 via MQTT");
|
||||||
ESP_LOGI(TAG, "Pump 2 turned ON");
|
esp_err_t ret = motor_start(MOTOR_PUMP_2, MOTOR_DEFAULT_SPEED);
|
||||||
mqtt_publish_pump_state(2, test_pump_2);
|
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) {
|
} else if (strncmp(data, "off", data_len) == 0) {
|
||||||
test_pump_2 = false;
|
ESP_LOGI(TAG, "Stopping pump 2 via MQTT");
|
||||||
ESP_LOGI(TAG, "Pump 2 turned OFF");
|
motor_stop(MOTOR_PUMP_2);
|
||||||
mqtt_publish_pump_state(2, test_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) {
|
} else if (strcmp(topic, TOPIC_CONFIG) == 0) {
|
||||||
ESP_LOGI(TAG, "Configuration update received");
|
ESP_LOGI(TAG, "Configuration update received");
|
||||||
// Parse JSON configuration here
|
// 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);
|
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)
|
static void sensor_simulation_task(void *pvParameters)
|
||||||
{
|
{
|
||||||
|
TickType_t last_stats_publish = 0;
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
// Wait for MQTT connection
|
// Wait for MQTT connection
|
||||||
if (mqtt_client_is_connected()) {
|
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%%",
|
ESP_LOGI(TAG, "Published moisture: Sensor1=%d%%, Sensor2=%d%%",
|
||||||
test_moisture_1, test_moisture_2);
|
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
|
// 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;
|
esp_chip_info_t chip_info;
|
||||||
|
|
||||||
@ -157,18 +575,18 @@ void app_main(void)
|
|||||||
// Print chip information
|
// Print chip information
|
||||||
print_chip_info();
|
print_chip_info();
|
||||||
|
|
||||||
// Print MQTT configuration
|
// Print configuration
|
||||||
|
ESP_LOGI(TAG, "Configuration:");
|
||||||
ESP_LOGI(TAG, " MQTT Broker: %s", CONFIG_MQTT_BROKER_URL);
|
ESP_LOGI(TAG, " MQTT Broker: %s", CONFIG_MQTT_BROKER_URL);
|
||||||
ESP_LOGI(TAG, "MQTT Username: %s", CONFIG_MQTT_USERNAME);
|
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
|
// Initialize WiFi manager
|
||||||
ESP_ERROR_CHECK(wifi_manager_init());
|
ESP_ERROR_CHECK(wifi_manager_init());
|
||||||
wifi_manager_register_callback(wifi_event_handler);
|
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
|
// Initialize OTA server
|
||||||
ESP_ERROR_CHECK(ota_server_init());
|
ESP_ERROR_CHECK(ota_server_init());
|
||||||
ota_server_set_version(APP_VERSION);
|
ota_server_set_version(APP_VERSION);
|
||||||
@ -180,6 +598,31 @@ void app_main(void)
|
|||||||
mqtt_disconnected_callback,
|
mqtt_disconnected_callback,
|
||||||
mqtt_data_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
|
// Start WiFi connection
|
||||||
esp_err_t ret = wifi_manager_start();
|
esp_err_t ret = wifi_manager_start();
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
@ -189,18 +632,51 @@ void app_main(void)
|
|||||||
// Create sensor simulation task
|
// Create sensor simulation task
|
||||||
xTaskCreate(sensor_simulation_task, "sensor_sim", 4096, NULL, 5, NULL);
|
xTaskCreate(sensor_simulation_task, "sensor_sim", 4096, NULL, 5, NULL);
|
||||||
|
|
||||||
|
// Create schedule status task
|
||||||
|
xTaskCreate(schedule_status_task, "schedule_status", 4096, NULL, 4, NULL);
|
||||||
|
|
||||||
// Main loop - monitor system status
|
// Main loop - monitor system status
|
||||||
while (1) {
|
while (1) {
|
||||||
ESP_LOGI(TAG, "System Status - WiFi: %s, MQTT: %s, Free heap: %d bytes",
|
ESP_LOGI(TAG, "System Status - WiFi: %s, MQTT: %s, Time: %s, Free heap: %d bytes",
|
||||||
wifi_manager_is_connected() ? "Connected" : "Disconnected",
|
wifi_manager_is_connected() ? "Connected" : "Disconnected",
|
||||||
mqtt_client_is_connected() ? "Connected" : "Disconnected",
|
mqtt_client_is_connected() ? "Connected" : "Disconnected",
|
||||||
|
scheduler_is_time_synchronized() ? "Synced" : "Not synced",
|
||||||
esp_get_free_heap_size());
|
esp_get_free_heap_size());
|
||||||
|
|
||||||
// Print pump states
|
// Print pump states and runtime
|
||||||
if (mqtt_client_is_connected()) {
|
if (mqtt_client_is_connected()) {
|
||||||
ESP_LOGI(TAG, "Pump States - Pump1: %s, Pump2: %s",
|
for (int i = 1; i <= 2; i++) {
|
||||||
test_pump_1 ? "ON" : "OFF",
|
motor_stats_t stats;
|
||||||
test_pump_2 ? "ON" : "OFF");
|
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
|
vTaskDelay(30000 / portTICK_PERIOD_MS); // Every 30 seconds
|
||||||
|
|||||||
739
main/motor_control.c
Normal file
739
main/motor_control.c
Normal 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
104
main/motor_control.h
Normal 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
242
main/motor_test.c
Normal 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, ¤t_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
871
main/scheduler.c
Normal 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
124
main/scheduler.h
Normal 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
99
mqtt_topic_plan.txt
Normal 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
217
pinout.svg
Normal 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
306
project_plan_v2.md
Normal 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
|
||||||
Reference in New Issue
Block a user