Compare commits

3 Commits

9 changed files with 1947 additions and 29 deletions

267
MOTOR_CONTROL_README.md Normal file
View File

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

2
cmd.txt Normal file
View File

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

View File

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

View File

@ -9,18 +9,59 @@
#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 "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.1.0-motor"
// 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; // 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);
}
}
// MQTT Callbacks // MQTT Callbacks
static void mqtt_connected_callback(void) static void mqtt_connected_callback(void)
@ -30,8 +71,42 @@ 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/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 +122,113 @@ 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 (strncmp(topic, "plant_watering/settings/pump/", 29) == 0) {
// Parse settings commands like:
// plant_watering/settings/pump/1/max_runtime
// plant_watering/settings/pump/1/min_interval
// plant_watering/settings/pump/1/min_speed
// plant_watering/settings/pump/1/max_speed
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) {
// Get current max speed to validate
motor_stats_t stats;
motor_get_stats(pump_id, &stats);
motor_set_speed_limits(pump_id, value, 100); // Assuming max stays at 100
ESP_LOGI(TAG, "Set pump %d min speed to %d%%", pump_id, value);
} 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 +267,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 +291,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,6 +321,33 @@ static void sensor_simulation_task(void *pvParameters)
} }
} }
// Task to demonstrate automated watering based on moisture
static void automation_demo_task(void *pvParameters)
{
bool auto_mode = false; // Start with manual mode
while (1) {
if (auto_mode && mqtt_client_is_connected()) {
// Simple threshold-based automation demo
if (test_moisture_1 < CONFIG_MOISTURE_THRESHOLD_LOW) {
if (!motor_is_running(MOTOR_PUMP_1) && !motor_is_cooldown(MOTOR_PUMP_1)) {
ESP_LOGI(TAG, "Auto: Moisture 1 low (%d%%), starting pump 1", test_moisture_1);
motor_start_timed(MOTOR_PUMP_1, MOTOR_DEFAULT_SPEED, 10000); // 10 second watering
}
}
if (test_moisture_2 < CONFIG_MOISTURE_THRESHOLD_LOW) {
if (!motor_is_running(MOTOR_PUMP_2) && !motor_is_cooldown(MOTOR_PUMP_2)) {
ESP_LOGI(TAG, "Auto: Moisture 2 low (%d%%), starting pump 2", test_moisture_2);
motor_start_timed(MOTOR_PUMP_2, MOTOR_DEFAULT_SPEED, 10000); // 10 second watering
}
}
}
vTaskDelay(30000 / portTICK_PERIOD_MS); // Check every 30 seconds
}
}
void print_chip_info(void) void print_chip_info(void)
{ {
esp_chip_info_t chip_info; esp_chip_info_t chip_info;
@ -157,18 +370,17 @@ void app_main(void)
// Print chip information // Print chip information
print_chip_info(); print_chip_info();
// Print MQTT configuration // Print configuration
ESP_LOGI(TAG, "MQTT Broker: %s", CONFIG_MQTT_BROKER_URL); ESP_LOGI(TAG, "Configuration:");
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 +392,27 @@ 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
// 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,6 +422,9 @@ void app_main(void)
// Create sensor simulation task // Create sensor simulation task
xTaskCreate(sensor_simulation_task, "sensor_sim", 4096, NULL, 5, NULL); xTaskCreate(sensor_simulation_task, "sensor_sim", 4096, NULL, 5, NULL);
// Create automation demo task (disabled by default)
xTaskCreate(automation_demo_task, "automation", 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, Free heap: %d bytes",
@ -196,11 +432,22 @@ void app_main(void)
mqtt_client_is_connected() ? "Connected" : "Disconnected", mqtt_client_is_connected() ? "Connected" : "Disconnected",
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);
}
} }
vTaskDelay(30000 / portTICK_PERIOD_MS); // Every 30 seconds vTaskDelay(30000 / portTICK_PERIOD_MS); // Every 30 seconds

739
main/motor_control.c Normal file
View File

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

104
main/motor_control.h Normal file
View File

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

242
main/motor_test.c Normal file
View File

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

99
mqtt_topic_plan.txt Normal file
View File

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

217
pinout.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 10 KiB