diff --git a/MOTOR_CONTROL_README.md b/MOTOR_CONTROL_README.md new file mode 100644 index 0000000..5839289 --- /dev/null +++ b/MOTOR_CONTROL_README.md @@ -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 -t "plant_watering/pump/1/set" -m "on" + +# Change speed +mosquitto_pub -h -t "plant_watering/pump/1/speed" -m "50" + +# Stop pump +mosquitto_pub -h -t "plant_watering/pump/1/set" -m "off" + +# Test run for 3 seconds +mosquitto_pub -h -t "plant_watering/commands/test_pump/1" -m "3000" + +# Emergency stop all +mosquitto_pub -h -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% + } +} +``` \ No newline at end of file diff --git a/cmd.txt b/cmd.txt new file mode 100644 index 0000000..f43c18d --- /dev/null +++ b/cmd.txt @@ -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 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index bc19edd..6ae7aa4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -5,6 +5,7 @@ idf_component_register( "ota_server.c" "plant_mqtt.c" "led_strip.c" + "motor_control.c" INCLUDE_DIRS "." REQUIRES diff --git a/main/main.c b/main/main.c index 721e60b..4c012b6 100644 --- a/main/main.c +++ b/main/main.c @@ -9,18 +9,56 @@ #include "wifi_manager.h" #include "ota_server.h" #include "plant_mqtt.h" +#include "motor_control.h" #include "sdkconfig.h" static const char *TAG = "MAIN"; // Application version -#define APP_VERSION "2.0.0-mqtt" +#define APP_VERSION "2.1.0-motor" // Test data static int test_moisture_1 = 45; static int test_moisture_2 = 62; -static bool test_pump_1 = false; -static bool test_pump_2 = false; + +// 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 static void mqtt_connected_callback(void) @@ -30,8 +68,23 @@ static void mqtt_connected_callback(void) // Publish initial states mqtt_publish_moisture(1, test_moisture_1); mqtt_publish_moisture(2, test_moisture_2); - mqtt_publish_pump_state(1, test_pump_1); - mqtt_publish_pump_state(2, test_pump_2); + mqtt_publish_pump_state(1, motor_is_running(MOTOR_PUMP_1)); + mqtt_publish_pump_state(2, motor_is_running(MOTOR_PUMP_2)); + + // Publish motor statistics + motor_stats_t stats; + for (int i = 1; i <= 2; i++) { + if (motor_get_stats(i, &stats) == ESP_OK) { + char topic[64]; + char data[128]; + + snprintf(topic, sizeof(topic), "plant_watering/pump/%d/stats", i); + snprintf(data, sizeof(data), + "{\"total_runtime\":%lu,\"run_count\":%lu,\"last_duration\":%lu}", + stats.total_runtime_ms, stats.run_count, stats.last_run_duration_ms); + mqtt_client_publish(topic, data, MQTT_QOS_0, MQTT_NO_RETAIN); + } + } } static void mqtt_disconnected_callback(void) @@ -47,27 +100,60 @@ static void mqtt_data_callback(const char* topic, const char* data, int data_len // Handle pump control commands if (strcmp(topic, TOPIC_PUMP_1_CMD) == 0) { if (strncmp(data, "on", data_len) == 0) { - test_pump_1 = true; - ESP_LOGI(TAG, "Pump 1 turned ON"); - mqtt_publish_pump_state(1, test_pump_1); + ESP_LOGI(TAG, "Starting pump 1 via MQTT"); + esp_err_t ret = motor_start(MOTOR_PUMP_1, MOTOR_DEFAULT_SPEED); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to start pump 1: %s", esp_err_to_name(ret)); + } } else if (strncmp(data, "off", data_len) == 0) { - test_pump_1 = false; - ESP_LOGI(TAG, "Pump 1 turned OFF"); - mqtt_publish_pump_state(1, test_pump_1); + ESP_LOGI(TAG, "Stopping pump 1 via MQTT"); + motor_stop(MOTOR_PUMP_1); + } else if (strncmp(data, "pulse", data_len) == 0) { + ESP_LOGI(TAG, "Pulse pump 1 for 5 seconds"); + motor_start_timed(MOTOR_PUMP_1, MOTOR_DEFAULT_SPEED, 5000); } } else if (strcmp(topic, TOPIC_PUMP_2_CMD) == 0) { if (strncmp(data, "on", data_len) == 0) { - test_pump_2 = true; - ESP_LOGI(TAG, "Pump 2 turned ON"); - mqtt_publish_pump_state(2, test_pump_2); + ESP_LOGI(TAG, "Starting pump 2 via MQTT"); + esp_err_t ret = motor_start(MOTOR_PUMP_2, MOTOR_DEFAULT_SPEED); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to start pump 2: %s", esp_err_to_name(ret)); + } } else if (strncmp(data, "off", data_len) == 0) { - test_pump_2 = false; - ESP_LOGI(TAG, "Pump 2 turned OFF"); - mqtt_publish_pump_state(2, test_pump_2); + ESP_LOGI(TAG, "Stopping pump 2 via MQTT"); + motor_stop(MOTOR_PUMP_2); + } else if (strncmp(data, "pulse", data_len) == 0) { + ESP_LOGI(TAG, "Pulse pump 2 for 5 seconds"); + motor_start_timed(MOTOR_PUMP_2, MOTOR_DEFAULT_SPEED, 5000); + } + } else if (strcmp(topic, "plant_watering/pump/1/speed") == 0) { + int speed = atoi(data); + if (speed >= 0 && speed <= 100) { + motor_set_speed(MOTOR_PUMP_1, speed); + ESP_LOGI(TAG, "Set pump 1 speed to %d%%", speed); + } + } else if (strcmp(topic, "plant_watering/pump/2/speed") == 0) { + int speed = atoi(data); + if (speed >= 0 && speed <= 100) { + motor_set_speed(MOTOR_PUMP_2, speed); + ESP_LOGI(TAG, "Set pump 2 speed to %d%%", speed); } } else if (strcmp(topic, TOPIC_CONFIG) == 0) { ESP_LOGI(TAG, "Configuration update received"); // Parse JSON configuration here + } else if (strcmp(topic, "plant_watering/commands/test_pump/1") == 0) { + uint32_t duration = atoi(data); + if (duration > 0 && duration <= 10000) { // Max 10 seconds for test + motor_test_run(MOTOR_PUMP_1, duration); + } + } else if (strcmp(topic, "plant_watering/commands/test_pump/2") == 0) { + uint32_t duration = atoi(data); + if (duration > 0 && duration <= 10000) { // Max 10 seconds for test + motor_test_run(MOTOR_PUMP_2, duration); + } + } else if (strcmp(topic, "plant_watering/commands/emergency_stop") == 0) { + ESP_LOGW(TAG, "Emergency stop command received!"); + motor_emergency_stop(); } } @@ -106,9 +192,11 @@ static void ota_progress_handler(int percent) ESP_LOGI(TAG, "OTA Progress: %d%%", percent); } -// Task to simulate sensor readings +// Task to simulate sensor readings and publish stats static void sensor_simulation_task(void *pvParameters) { + TickType_t last_stats_publish = 0; + while (1) { // Wait for MQTT connection if (mqtt_client_is_connected()) { @@ -128,6 +216,29 @@ static void sensor_simulation_task(void *pvParameters) ESP_LOGI(TAG, "Published moisture: Sensor1=%d%%, Sensor2=%d%%", test_moisture_1, test_moisture_2); + + // Publish pump runtime stats every minute + if (xTaskGetTickCount() - last_stats_publish > pdMS_TO_TICKS(60000)) { + last_stats_publish = xTaskGetTickCount(); + + for (int i = 1; i <= 2; i++) { + // Publish current runtime if running + if (motor_is_running(i)) { + char topic[64]; + char data[32]; + snprintf(topic, sizeof(topic), "plant_watering/pump/%d/runtime", i); + snprintf(data, sizeof(data), "%lu", motor_get_runtime_ms(i)); + mqtt_client_publish(topic, data, MQTT_QOS_0, MQTT_NO_RETAIN); + } + + // Publish cooldown status + if (motor_is_cooldown(i)) { + char topic[64]; + snprintf(topic, sizeof(topic), "plant_watering/pump/%d/cooldown", i); + mqtt_client_publish(topic, "true", MQTT_QOS_0, MQTT_NO_RETAIN); + } + } + } } // Update every 10 seconds @@ -135,6 +246,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) { esp_chip_info_t chip_info; @@ -157,18 +295,17 @@ void app_main(void) // Print chip information print_chip_info(); - // Print MQTT configuration - ESP_LOGI(TAG, "MQTT Broker: %s", CONFIG_MQTT_BROKER_URL); - ESP_LOGI(TAG, "MQTT Username: %s", CONFIG_MQTT_USERNAME); + // Print configuration + ESP_LOGI(TAG, "Configuration:"); + ESP_LOGI(TAG, " Moisture threshold low: %d%%", CONFIG_MOISTURE_THRESHOLD_LOW); + ESP_LOGI(TAG, " Moisture threshold high: %d%%", CONFIG_MOISTURE_THRESHOLD_HIGH); + ESP_LOGI(TAG, " Max watering duration: %d ms", CONFIG_WATERING_MAX_DURATION_MS); + ESP_LOGI(TAG, " Min watering interval: %d ms", CONFIG_WATERING_MIN_INTERVAL_MS); // Initialize WiFi manager ESP_ERROR_CHECK(wifi_manager_init()); wifi_manager_register_callback(wifi_event_handler); - // TEMPORARY: Clear stored credentials to force use of new ones - // wifi_manager_clear_credentials(); - // ESP_LOGI(TAG, "Cleared stored WiFi credentials"); - // Initialize OTA server ESP_ERROR_CHECK(ota_server_init()); ota_server_set_version(APP_VERSION); @@ -180,6 +317,29 @@ void app_main(void) mqtt_disconnected_callback, mqtt_data_callback); + // Initialize Motor Control + ESP_ERROR_CHECK(motor_control_init()); + motor_register_state_callback(motor_state_change_callback); + motor_register_error_callback(motor_error_callback); + + // Configure motor safety limits 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); + + // Subscribe to additional MQTT topics after connection + static const char* additional_topics[] = { + "plant_watering/pump/+/speed", + "plant_watering/commands/test_pump/+", + "plant_watering/commands/emergency_stop", + "plant_watering/settings/+/+", + NULL + }; + + // This would need to be done after MQTT connection + // You might want to add this to the mqtt_connected_callback + // Start WiFi connection esp_err_t ret = wifi_manager_start(); if (ret != ESP_OK) { @@ -189,6 +349,9 @@ void app_main(void) // Create sensor simulation task 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 while (1) { ESP_LOGI(TAG, "System Status - WiFi: %s, MQTT: %s, Free heap: %d bytes", @@ -196,11 +359,22 @@ void app_main(void) mqtt_client_is_connected() ? "Connected" : "Disconnected", esp_get_free_heap_size()); - // Print pump states + // Print pump states and runtime if (mqtt_client_is_connected()) { - ESP_LOGI(TAG, "Pump States - Pump1: %s, Pump2: %s", - test_pump_1 ? "ON" : "OFF", - test_pump_2 ? "ON" : "OFF"); + for (int i = 1; i <= 2; i++) { + motor_stats_t stats; + motor_get_stats(i, &stats); + + const char *state_str = "OFF"; + if (motor_is_running(i)) { + state_str = "ON"; + } else if (motor_is_cooldown(i)) { + state_str = "COOLDOWN"; + } + + ESP_LOGI(TAG, "Pump %d: %s, Total runtime: %lu s, Runs: %lu", + i, state_str, stats.total_runtime_ms / 1000, stats.run_count); + } } vTaskDelay(30000 / portTICK_PERIOD_MS); // Every 30 seconds diff --git a/main/motor_control.c b/main/motor_control.c new file mode 100644 index 0000000..60fcef7 --- /dev/null +++ b/main/motor_control.c @@ -0,0 +1,712 @@ +#include +#include +#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++) { + s_motors[i].safety_timer = xTimerCreate("motor_safety", + 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; + } + + s_motors[i].soft_start_timer = xTimerCreate("motor_soft_start", + 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); + + ESP_LOGW(TAG, "Safety timer expired for motor %d", id); + motor_stop(id); + + if (s_error_callback) { + s_error_callback(id, "Maximum runtime exceeded"); + } +} + +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; + } + + // Ramp up speed + if (motor->speed_percent < motor->target_speed) { + motor->speed_percent += 5; // 5% 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; + motor_update_pwm(id, duty); + + // Stop timer when target reached + if (motor->speed_percent >= motor->target_speed) { + xTimerStop(xTimer, 0); + } + } +} + +// 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; +} \ No newline at end of file diff --git a/main/motor_control.h b/main/motor_control.h new file mode 100644 index 0000000..31b8b7a --- /dev/null +++ b/main/motor_control.h @@ -0,0 +1,97 @@ +#ifndef MOTOR_CONTROL_H +#define MOTOR_CONTROL_H + +#include +#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 (%) +#define MOTOR_MAX_RUNTIME_MS 30000 // Maximum runtime (30 seconds) +#define MOTOR_MIN_INTERVAL_MS 300000 // Minimum interval between runs (5 minutes) +#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 \ No newline at end of file diff --git a/mqtt_topic_plan.txt b/mqtt_topic_plan.txt new file mode 100644 index 0000000..4b13cc5 --- /dev/null +++ b/mqtt_topic_plan.txt @@ -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 + diff --git a/pinout.svg b/pinout.svg new file mode 100644 index 0000000..931bdd4 --- /dev/null +++ b/pinout.svg @@ -0,0 +1,217 @@ + + + ESP32-S3-MINI-1 Plant Watering System + + + + + ESP32-S3-MINI-1 + + + 3V3 + + + GND + + + GPIO4 + + (AIN1) + + GPIO5 + + (AIN2) + + GPIO6 + + (BIN1) + + GPIO7 + + (BIN2) + + GPIO8 + + (PWMA) + + GPIO9 + + (PWMB) + + GPIO10 + + (STBY) + + + GPIO1 + + (ADC1) + + GPIO2 + + (ADC2) + + + + + + TB6612FNG + + + VM + + + VCC + + + GND + + + AIN1 + + + AIN2 + + + BIN1 + + + BIN2 + + + PWMA + + + PWMB + + + STBY + + + + A01 + + + A02 + + + B01 + + + B02 + + + + + + + Pump 1 + 12V DC + + + + + + + + Pump 2 + 12V DC + + + + + + + + Soil Sensor 1 + VCC + + GND + + SIG + + + + + + + Soil Sensor 2 + VCC + + GND + + SIG + + + + + + + 12V Power + 12V DC + 2A min + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Wire Colors + + Power (3.3V/12V) + + Ground + + Direction Control + + PWM Speed + + Standby + + Analog Signal + + Motor Output + + + + Notes: + • VM (Motor Voltage): 12V DC for pumps | • VCC (Logic Voltage): 3.3V from ESP32 | • STBY must be HIGH to enable motors + \ No newline at end of file