From 5fd11369cc449d7ab43c6bed33c9bf1cfd0844b9 Mon Sep 17 00:00:00 2001 From: Stephen Minakian Date: Thu, 17 Jul 2025 20:41:25 -0600 Subject: [PATCH] Added MQTT --- README.md | 371 ++++++++++++++++++----------------- main/CMakeLists.txt | 2 + main/main.c | 210 +++++++++++--------- main/plant_mqtt.c | 457 ++++++++++++++++++++++++++++++++++++++++++++ main/plant_mqtt.h | 86 +++++++++ 5 files changed, 858 insertions(+), 268 deletions(-) create mode 100644 main/plant_mqtt.c create mode 100644 main/plant_mqtt.h diff --git a/README.md b/README.md index b310e0c..d2f44a0 100644 --- a/README.md +++ b/README.md @@ -1,232 +1,249 @@ -# ESP32-S3 WiFi OTA Template +# ESP32-S3 Plant Watering System -A reusable template for ESP32-S3 projects featuring WiFi connectivity and Over-The-Air (OTA) firmware updates. This project is designed for the SparkFun ESP32-S3 Thing Plus but can be adapted for other ESP32-S3 boards. +An automated plant watering system built with ESP32-S3, featuring MQTT communication, OTA updates, and remote monitoring/control capabilities. -## Features +## Current Project Status -- 🌐 **WiFi Manager** - Automatic connection with credential storage in NVS -- 🔄 **OTA Updates** - Web-based firmware updates with drag-and-drop interface -- 💡 **RGB LED Control** - WS2812 driver for visual feedback (GPIO 46) -- 🔧 **Docker-based Development** - No local ESP-IDF installation required -- 📦 **Modular Design** - Easy to extend with additional features +### ✅ Completed Features +- **WiFi Manager**: Auto-connect with NVS credential storage +- **OTA Updates**: Web-based firmware updates via HTTP server +- **MQTT Client**: Full MQTT integration with NVS credential storage + - Auto-reconnection + - Last Will and Testament (LWT) + - Command subscription + - Sensor data publishing -## Hardware Requirements +### 🚧 In Progress +- Motor control (TB6612FNG driver) +- Moisture sensor reading +- Automation logic -- SparkFun ESP32-S3 Thing Plus (or compatible ESP32-S3 board) -- USB-C cable (data capable, not charge-only) -- 2.4GHz WiFi network +### 📋 TODO +- Web dashboard +- Home Assistant integration +- Multiple zone support -## Project Structure +## Hardware +- **MCU**: ESP32-S3-MINI-1 +- **Motor Driver**: TB6612FNG (for 2 water pumps) +- **Sensors**: 2x Capacitive soil moisture sensors +- **Power**: 12V supply for pumps, 3.3V for logic + +## Software Architecture + +### MQTT Topics + +| Topic | Direction | Description | Example | +|-------|-----------|-------------|---------| +| `plant_watering/status` | Publish | System online/offline status | "online" | +| `plant_watering/moisture/1` | Publish | Moisture sensor 1 reading (%) | "45" | +| `plant_watering/moisture/2` | Publish | Moisture sensor 2 reading (%) | "62" | +| `plant_watering/pump/1/set` | Subscribe | Pump 1 control command | "on"/"off" | +| `plant_watering/pump/2/set` | Subscribe | Pump 2 control command | "on"/"off" | +| `plant_watering/pump/1/state` | Publish | Pump 1 current state | "on"/"off" | +| `plant_watering/pump/2/state` | Publish | Pump 2 current state | "on"/"off" | +| `plant_watering/config` | Subscribe | Configuration updates | JSON config | + +## Configuration + +### WiFi Settings (menuconfig) ``` -. -├── main/ -│ ├── main.c # Main application -│ ├── wifi_manager.c/h # WiFi connection management -│ ├── ota_server.c/h # OTA update server -│ ├── led_strip.c/h # RGB LED driver -│ ├── CMakeLists.txt # Component configuration -| └── Kconfig.projbuild # To store wifi ssid and pass -├── partitions.csv # Flash partition table (4MB) -├── sdkconfig # Project configuration (auto-generated) -├── .gitignore # Git ignore rules -└── README.md # This file +CONFIG_WIFI_SSID="Your_SSID" +CONFIG_WIFI_PASSWORD="Your_Password" ``` -## Quick Start +### MQTT Settings (menuconfig) +``` +CONFIG_MQTT_BROKER_URL="mqtt://192.168.1.100:1883" +CONFIG_MQTT_USERNAME="plantwater" +CONFIG_MQTT_PASSWORD="your_password" +``` -### 1. Prerequisites +### Plant Watering Settings (menuconfig) +``` +CONFIG_MOISTURE_THRESHOLD_LOW=30 # Start watering below this % +CONFIG_MOISTURE_THRESHOLD_HIGH=70 # Stop watering at this % +CONFIG_WATERING_MAX_DURATION_MS=30000 # Max pump runtime (30s) +CONFIG_WATERING_MIN_INTERVAL_MS=300000 # Min time between watering (5min) +``` -- Docker installed on your system -- Git for version control -- Terminal/command line access +## Building and Flashing -### 2. Clone and Configure +### Using Docker (Recommended) +#### Configure the project ```bash -# Clone the repository (or create new project) -git clone -cd esp32-s3-template - -# Create the file main/Kconfig.projbuild -touch main/Kconfig.projbuild -``` -Fill it in with the following: -``` -menu "Wi-Fi Configuration" - -config WIFI_SSID - string "WiFi SSID" - default "" - help - The SSID of the WiFi network. - -config WIFI_PASSWORD - string "WiFi Password" - default "" - help - The password of the WiFi network. - -endmenu -``` -Then fill in your SSID and Password - -A new project can be started with: -```bash -docker run --user $(id -u):$(id -g) --rm -v $PWD:/project -w /project -it espressif/idf:latest idf.py create-project +docker run --user $(id -u):$(id -g) --rm -v $PWD:/project -w /project -it espressif/idf:latest idf.py menuconfig ``` -### 3. Build the Project - +#### Build ```bash docker run --user $(id -u):$(id -g) --rm -v $PWD:/project -w /project -it espressif/idf:latest idf.py build ``` -### 4. Flash to Device - +#### Flash via USB ```bash docker run --privileged --rm -v $PWD:/project -w /project --device=/dev/ttyACM0 -it espressif/idf:latest idf.py flash -p /dev/ttyACM0 ``` -> **Note**: Your device might appear as `/dev/ttyUSB0` or another port. Check with `ls /dev/tty*` after connecting. - -### 5. Monitor Serial Output - +#### Monitor serial output ```bash docker run --privileged --rm -v $PWD:/project -w /project --device=/dev/ttyACM0 -it espressif/idf:latest idf.py monitor -p /dev/ttyACM0 ``` -Press `Ctrl+]` to exit the monitor. - -## Using OTA Updates - -1. **Connect to WiFi** - The device will automatically connect using stored credentials -2. **Find IP Address** - Check serial monitor for "Got IP: xxx.xxx.xxx.xxx" -3. **Open Web Interface** - Navigate to `http:///` in your browser -4. **Upload Firmware**: - - Build new version: Update `APP_VERSION` in main.c - - Run build command again - - Upload `build/.bin` via web interface - - Device will automatically restart with new firmware - -### Testing OTA Updates - -Try these modifications to test OTA: - -```c -// Version 2.0.0 - Faster blinking -#define APP_VERSION "2.0.0" -#define BLINK_DELAY_MS 200 // Was 500 - -// Version 3.0.0 - Different colors -static const color_t colors[] = { - {255, 128, 0, "Orange"}, - {128, 0, 255, "Purple"}, - {255, 192, 203, "Pink"}, -}; +#### Flash and monitor in one command +```bash +docker run --privileged --rm -v $PWD:/project -w /project --device=/dev/ttyACM0 -it espressif/idf:latest idf.py flash monitor -p /dev/ttyACM0 ``` -## API Usage +Note: Replace `/dev/ttyACM0` with your actual device port (could be `/dev/ttyUSB0`, `/dev/ttyACM1`, etc.) -### WiFi Manager +### Using Local ESP-IDF Installation -```c -// Set new credentials -wifi_manager_set_credentials("NewSSID", "NewPassword"); +```bash +# Configure the project +idf.py menuconfig -// Check connection status -if (wifi_manager_is_connected()) { - // Connected -} +# Build +idf.py build -// Clear stored credentials -wifi_manager_clear_credentials(); +# Flash via USB +idf.py -p /dev/ttyUSB0 flash monitor ``` -### OTA Server +### OTA Updates +1. Connect to the same network as the ESP32 +2. Navigate to `http:///` +3. Upload the `build/PlantWater.bin` file +4. Device will automatically restart with new firmware -```c -// Set version string -ota_server_set_version("2.0.0"); +## Testing with MQTT -// Register progress callback -ota_server_register_progress_callback(my_progress_handler); +### Monitor All Topics +```bash +# Using Docker +docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 \ + mosquitto_sub -h mosquitto -u monitor -P password -t "plant_watering/#" -v + +# Using local mosquitto +mosquitto_sub -h 192.168.1.100 -u monitor -P password -t "plant_watering/#" -v ``` -### LED Control +### Control Pumps +```bash +# Turn Pump 1 ON +docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 \ + mosquitto_pub -h mosquitto -u home-server -P password -t "plant_watering/pump/1/set" -m "on" -```c -// Set LED color -led_strip_set_pixel(strip, 0, 255, 0, 0); // Red -led_strip_refresh(strip); +# Turn Pump 1 OFF +docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 \ + mosquitto_pub -h mosquitto -u home-server -P password -t "plant_watering/pump/1/set" -m "off" -// Turn off -led_strip_clear(strip); +# Turn Pump 2 ON +docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 \ + mosquitto_pub -h mosquitto -u home-server -P password -t "plant_watering/pump/2/set" -m "on" + +# Turn Pump 2 OFF +docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 \ + mosquitto_pub -h mosquitto -u home-server -P password -t "plant_watering/pump/2/set" -m "off" ``` +### Local mosquitto commands (if installed) +```bash +# Subscribe to all topics +mosquitto_sub -h 192.168.1.100 -u monitor -P password -t "plant_watering/#" -v + +# Control pumps +mosquitto_pub -h 192.168.1.100 -u home-server -P password -t "plant_watering/pump/1/set" -m "on" +mosquitto_pub -h 192.168.1.100 -u home-server -P password -t "plant_watering/pump/1/set" -m "off" +``` + +## Current Behavior + +When the system is running: +1. **On boot**: Connects to WiFi, then MQTT broker +2. **Status**: Publishes "online" to `plant_watering/status` +3. **Sensors**: Publishes simulated moisture readings every 10 seconds +4. **Commands**: Responds to pump on/off commands +5. **Feedback**: Publishes pump state changes to state topics +6. **Disconnect**: LWT publishes "offline" to status topic + +## Project Structure + +``` +main/ +├── CMakeLists.txt # Build configuration +├── Kconfig.projbuild # menuconfig options +├── main.c # Main application +├── wifi_manager.c/h # WiFi connection management +├── ota_server.c/h # OTA update server +├── plant_mqtt.c/h # MQTT client implementation +├── led_strip.c/h # RGB LED control (from template) +├── motor_control.c/h # (TODO) Pump motor control +└── moisture_sensor.c/h # (TODO) Sensor reading +``` + +## Credential Management + +Both WiFi and MQTT credentials are stored in NVS (Non-Volatile Storage): +- **First boot**: Uses menuconfig defaults and saves to NVS +- **Subsequent boots**: Loads from NVS +- **OTA updates**: Preserves NVS (credentials survive updates) + +To update credentials after deployment: +1. Change in menuconfig +2. Add temporary force-update code +3. Build and OTA update +4. Remove temporary code and update again + +Or erase flash completely: `idf.py erase-flash` + +## Version History + +- **v2.0.0-mqtt**: Added MQTT client with NVS storage +- **v1.0.1**: Initial OTA-enabled template +- **v1.0.0**: Basic LED blink example + ## Troubleshooting -### Build Issues -- Ensure Docker is running and you have internet connection -- Clean build: `idf.py fullclean` before building - -### Flash Issues -- Check USB cable is data-capable (not charge-only) -- Try different USB port -- Verify device path (`/dev/ttyACM0`, `/dev/ttyUSB0`, etc.) -- May need to add user to `dialout` group: `sudo usermod -a -G dialout $USER` +### MQTT Connection Issues +- Check broker is running: `docker ps` +- Verify credentials match broker configuration +- Ensure ESP32 and broker are on same network +- Check firewall rules for port 1883 ### WiFi Connection Issues -- Verify 2.4GHz network (ESP32 doesn't support 5GHz) -- Check for special characters in SSID/password -- Look for trailing spaces in SSID -- Monitor serial output for specific error codes +- Verify SSID has no trailing spaces +- Check password is correct +- Ensure 2.4GHz network (ESP32 doesn't support 5GHz) +- Try erasing flash and reflashing -### OTA Issues -- Ensure device has sufficient free space (check web interface) -- Verify binary file size fits in OTA partition (1.25MB max) -- Check same network connectivity between computer and ESP32 +### OTA Update Issues +- Ensure device is connected to network +- Check partition table has OTA partitions +- Verify firmware size fits in OTA partition +- Try accessing `http:///test` to verify server -## Memory Layout +## Next Development Steps -| Partition | Type | Size | Purpose | -|-----------|---------|---------|-------------------| -| nvs | data | 16KB | WiFi credentials | -| otadata | data | 8KB | OTA selection | -| phy_init | data | 4KB | PHY calibration | -| factory | app | 1.25MB | Factory firmware | -| ota_0 | app | 1.25MB | OTA partition 1 | -| ota_1 | app | 1.25MB | OTA partition 2 | +1. **Motor Control Module** + - PWM speed control + - Safety timeouts + - Current monitoring -## Security Considerations +2. **Moisture Sensor Module** + - ADC calibration + - Averaging/filtering + - Percentage conversion -For production deployments: -- Add authentication to OTA web interface -- Use HTTPS for OTA updates -- Implement firmware signature verification -- Store WiFi credentials securely -- Consider encrypted flash storage +3. **Automation Logic** + - Threshold-based watering + - Time-based schedules + - Prevent overwatering -## Extending the Template - -This template provides core functionality. Add your application-specific features: - -1. **Remove LED code** if not using RGB LED -2. **Add sensors** - I2C/SPI initialization in main.c -3. **Add MQTT** - Build on WiFi manager callbacks -4. **Add web API** - Extend OTA server with custom endpoints -5. **Add BLE** - ESP32-S3 supports dual-mode - -## License - -[Your License Here] - -## Acknowledgments - -- Built with ESP-IDF v6.0 -- Designed for SparkFun ESP32-S3 Thing Plus -- RGB LED driver uses RMT peripheral - ---- - -For more information about ESP-IDF: https://docs.espressif.com/ \ No newline at end of file +4. **Enhanced Features** + - Web dashboard + - Historical data logging + - Multi-zone support + - Weather API integration \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 3d0c579..bc19edd 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -3,6 +3,7 @@ idf_component_register( "main.c" "wifi_manager.c" "ota_server.c" + "plant_mqtt.c" "led_strip.c" INCLUDE_DIRS "." @@ -12,4 +13,5 @@ idf_component_register( esp_http_server app_update driver + mqtt ) \ No newline at end of file diff --git a/main/main.c b/main/main.c index df22002..721e60b 100644 --- a/main/main.c +++ b/main/main.c @@ -5,54 +5,89 @@ #include "esp_system.h" #include "esp_log.h" #include "esp_chip_info.h" +#include "esp_random.h" #include "wifi_manager.h" #include "ota_server.h" -#include "led_strip.h" - +#include "plant_mqtt.h" #include "sdkconfig.h" static const char *TAG = "MAIN"; -// WiFi credentials - Change these to your network -// #define WIFI_SSID "YOUR_SSID" -// #define WIFI_PASSWORD "YOUR_PASSWORD" - -const char *ssid = CONFIG_WIFI_SSID; -const char *password = CONFIG_WIFI_PASSWORD; - // Application version -#define APP_VERSION "1.0.1" +#define APP_VERSION "2.0.0-mqtt" -// LED colors and timing -typedef struct { - uint8_t r, g, b; - const char *name; -} color_t; +// 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; -static const color_t colors[] = { - {255, 0, 0, "Red"}, - {0, 255, 0, "Green"}, - {0, 0, 255, "Blue"}, - {255, 255, 0, "Yellow"}, - {255, 0, 255, "Magenta"}, - {0, 255, 255, "Cyan"}, - {255, 255, 255, "White"}, -}; +// MQTT Callbacks +static void mqtt_connected_callback(void) +{ + ESP_LOGI(TAG, "MQTT Connected - Publishing initial status"); + + // 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); +} -#define NUM_COLORS (sizeof(colors) / sizeof(colors[0])) -#define BLINK_DELAY_MS 200 +static void mqtt_disconnected_callback(void) +{ + ESP_LOGW(TAG, "MQTT Disconnected"); +} + +static void mqtt_data_callback(const char* topic, const char* data, int data_len) +{ + ESP_LOGI(TAG, "MQTT Data received on topic: %s", topic); + ESP_LOGI(TAG, "Data: %.*s", data_len, data); + + // 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); + } 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); + } + } 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); + } 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); + } + } else if (strcmp(topic, TOPIC_CONFIG) == 0) { + ESP_LOGI(TAG, "Configuration update received"); + // Parse JSON configuration here + } +} // WiFi event handler static void wifi_event_handler(wifi_state_t state) { switch (state) { case WIFI_STATE_CONNECTED: - ESP_LOGI(TAG, "WiFi connected - starting OTA server"); + ESP_LOGI(TAG, "WiFi connected - starting services"); ota_server_start(); + + // Start MQTT client + if (mqtt_client_start() != ESP_OK) { + ESP_LOGE(TAG, "Failed to start MQTT client"); + } break; case WIFI_STATE_DISCONNECTED: - ESP_LOGW(TAG, "WiFi disconnected - stopping OTA server"); + ESP_LOGW(TAG, "WiFi disconnected - stopping services"); + mqtt_client_stop(); ota_server_stop(); break; @@ -71,6 +106,35 @@ static void ota_progress_handler(int percent) ESP_LOGI(TAG, "OTA Progress: %d%%", percent); } +// Task to simulate sensor readings +static void sensor_simulation_task(void *pvParameters) +{ + while (1) { + // Wait for MQTT connection + if (mqtt_client_is_connected()) { + // Simulate moisture sensor readings with some variation + test_moisture_1 += (esp_random() % 5) - 2; // +/- 2 + test_moisture_2 += (esp_random() % 5) - 2; // +/- 2 + + // Keep values in range + if (test_moisture_1 < 0) test_moisture_1 = 0; + if (test_moisture_1 > 100) test_moisture_1 = 100; + if (test_moisture_2 < 0) test_moisture_2 = 0; + if (test_moisture_2 > 100) test_moisture_2 = 100; + + // Publish sensor data + mqtt_publish_moisture(1, test_moisture_1); + mqtt_publish_moisture(2, test_moisture_2); + + ESP_LOGI(TAG, "Published moisture: Sensor1=%d%%, Sensor2=%d%%", + test_moisture_1, test_moisture_2); + } + + // Update every 10 seconds + vTaskDelay(10000 / portTICK_PERIOD_MS); + } +} + void print_chip_info(void) { esp_chip_info_t chip_info; @@ -83,55 +147,38 @@ void print_chip_info(void) (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : ""); ESP_LOGI(TAG, "silicon revision %d, ", chip_info.revision); - ESP_LOGI(TAG, "Minimum free heap size: %d bytes", esp_get_minimum_free_heap_size()); } void app_main(void) { - ESP_LOGI(TAG, "ESP32-S3 Thing Plus RGB Blinker v%s", APP_VERSION); + ESP_LOGI(TAG, "Plant Watering System v%s", APP_VERSION); // Print chip information print_chip_info(); - // Initialize RGB LED - led_strip_t *led_strip = led_strip_init(LED_STRIP_GPIO, LED_STRIP_LED_COUNT); - if (!led_strip) { - ESP_LOGE(TAG, "Failed to initialize LED strip"); - } else { - ESP_LOGI(TAG, "RGB LED initialized on GPIO %d", LED_STRIP_GPIO); - // Turn LED off initially - led_strip_clear(led_strip); - } + // Print MQTT configuration + ESP_LOGI(TAG, "MQTT Broker: %s", CONFIG_MQTT_BROKER_URL); + ESP_LOGI(TAG, "MQTT Username: %s", CONFIG_MQTT_USERNAME); // 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); ota_server_register_progress_callback(ota_progress_handler); - // Check if we have stored WiFi credentials - char stored_ssid[33] = {0}; - char stored_pass[65] = {0}; - - // Force update with new credentials (remove this after first successful connection) - ESP_LOGI(TAG, "Updating WiFi credentials - SSID: '%s'", ssid); - wifi_manager_set_credentials(ssid, password); - - /* - // Normal flow - only update if no credentials stored - if (wifi_manager_get_credentials(stored_ssid, sizeof(stored_ssid), - stored_pass, sizeof(stored_pass)) != ESP_OK) { - ESP_LOGI(TAG, "No stored WiFi credentials, saving default ones"); - ESP_LOGI(TAG, "Setting SSID: '%s'", ssid); - wifi_manager_set_credentials(ssid, password); - } else { - ESP_LOGI(TAG, "Found stored credentials - SSID: '%s'", stored_ssid); - } - */ + // Initialize MQTT client + ESP_ERROR_CHECK(mqtt_client_init()); + mqtt_client_register_callbacks(mqtt_connected_callback, + mqtt_disconnected_callback, + mqtt_data_callback); // Start WiFi connection esp_err_t ret = wifi_manager_start(); @@ -139,42 +186,23 @@ void app_main(void) ESP_LOGE(TAG, "Failed to start WiFi manager"); } - // Main loop with RGB LED blinking - int color_index = 0; - bool led_on = false; + // Create sensor simulation task + xTaskCreate(sensor_simulation_task, "sensor_sim", 4096, NULL, 5, NULL); + // Main loop - monitor system status while (1) { - // Blink the RGB LED through different colors - if (led_strip) { - if (led_on) { - // Turn LED on with current color - led_strip_set_pixel(led_strip, 0, - colors[color_index].r, - colors[color_index].g, - colors[color_index].b); - led_strip_refresh(led_strip); - ESP_LOGI(TAG, "LED ON - Color: %s", colors[color_index].name); - } else { - // Turn LED off - led_strip_clear(led_strip); - ESP_LOGI(TAG, "LED OFF"); - - // Move to next color when turning off - color_index = (color_index + 1) % NUM_COLORS; - } - - led_on = !led_on; + ESP_LOGI(TAG, "System Status - WiFi: %s, MQTT: %s, Free heap: %d bytes", + wifi_manager_is_connected() ? "Connected" : "Disconnected", + mqtt_client_is_connected() ? "Connected" : "Disconnected", + esp_get_free_heap_size()); + + // Print pump states + if (mqtt_client_is_connected()) { + ESP_LOGI(TAG, "Pump States - Pump1: %s, Pump2: %s", + test_pump_1 ? "ON" : "OFF", + test_pump_2 ? "ON" : "OFF"); } - // Print heap info every 10 blinks (5 seconds) - static int blink_count = 0; - if (++blink_count >= 10) { - ESP_LOGI(TAG, "Free heap: %d bytes, WiFi: %s", - esp_get_free_heap_size(), - wifi_manager_is_connected() ? "Connected" : "Disconnected"); - blink_count = 0; - } - - vTaskDelay(BLINK_DELAY_MS / portTICK_PERIOD_MS); + vTaskDelay(30000 / portTICK_PERIOD_MS); // Every 30 seconds } } \ No newline at end of file diff --git a/main/plant_mqtt.c b/main/plant_mqtt.c new file mode 100644 index 0000000..14b2f8e --- /dev/null +++ b/main/plant_mqtt.c @@ -0,0 +1,457 @@ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_system.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "plant_mqtt.h" +#include "mqtt_client.h" // ESP-IDF MQTT client header + +static const char *TAG = "MQTT_CLIENT"; + +// NVS namespace for MQTT settings +#define MQTT_NVS_NAMESPACE "mqtt_config" + +// MQTT client handle +static esp_mqtt_client_handle_t s_mqtt_client = NULL; + +// Current state +static mqtt_state_t s_mqtt_state = MQTT_STATE_DISCONNECTED; + +// Callbacks +static mqtt_connected_callback_t s_connected_callback = NULL; +static mqtt_disconnected_callback_t s_disconnected_callback = NULL; +static mqtt_data_callback_t s_data_callback = NULL; + +// Mutex for thread safety +static SemaphoreHandle_t s_mqtt_mutex = NULL; + +// Forward declarations +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); +static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event); + +esp_err_t mqtt_client_init(void) +{ + if (s_mqtt_client != NULL) { + ESP_LOGW(TAG, "MQTT client already initialized"); + return ESP_OK; + } + + // Create mutex + s_mqtt_mutex = xSemaphoreCreateMutex(); + if (s_mqtt_mutex == NULL) { + ESP_LOGE(TAG, "Failed to create mutex"); + return ESP_FAIL; + } + + // Try to load credentials from NVS + char url[128] = {0}; + char username[64] = {0}; + char password[64] = {0}; + + if (mqtt_client_get_config(url, sizeof(url), username, sizeof(username), + password, sizeof(password)) != ESP_OK) { + // Use defaults from menuconfig + ESP_LOGI(TAG, "No stored MQTT config, using defaults from menuconfig"); + strlcpy(url, CONFIG_MQTT_BROKER_URL, sizeof(url)); + strlcpy(username, CONFIG_MQTT_USERNAME, sizeof(username)); + strlcpy(password, CONFIG_MQTT_PASSWORD, sizeof(password)); + + // Save defaults to NVS + mqtt_client_set_broker_url(url); + mqtt_client_set_credentials(username, password); + } else { + ESP_LOGI(TAG, "Loaded MQTT config from NVS"); + } + + // Configure MQTT client - ESP-IDF v5+ format + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.uri = url, + .credentials.username = username, + .credentials.authentication.password = password, + .credentials.client_id = MQTT_CLIENT_ID, + .session.keepalive = MQTT_KEEPALIVE, + .session.last_will.topic = TOPIC_LAST_WILL, + .session.last_will.msg = STATUS_OFFLINE, + .session.last_will.qos = MQTT_QOS_1, + .session.last_will.retain = MQTT_RETAIN, + .network.reconnect_timeout_ms = 10000, + }; + + // Create MQTT client + s_mqtt_client = esp_mqtt_client_init(&mqtt_cfg); + if (s_mqtt_client == NULL) { + ESP_LOGE(TAG, "Failed to create MQTT client"); + vSemaphoreDelete(s_mqtt_mutex); + s_mqtt_mutex = NULL; + return ESP_FAIL; + } + + // Register event handler + esp_err_t ret = esp_mqtt_client_register_event(s_mqtt_client, ESP_EVENT_ANY_ID, + mqtt_event_handler, s_mqtt_client); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to register event handler"); + esp_mqtt_client_destroy(s_mqtt_client); + s_mqtt_client = NULL; + vSemaphoreDelete(s_mqtt_mutex); + s_mqtt_mutex = NULL; + return ret; + } + + ESP_LOGI(TAG, "MQTT client initialized"); + return ESP_OK; +} + +esp_err_t mqtt_client_start(void) +{ + if (s_mqtt_client == NULL) { + ESP_LOGE(TAG, "MQTT client not initialized"); + return ESP_ERR_INVALID_STATE; + } + + ESP_LOGI(TAG, "Starting MQTT client..."); + ESP_LOGI(TAG, "Broker URL: %s", CONFIG_MQTT_BROKER_URL); + ESP_LOGI(TAG, "Client ID: %s", MQTT_CLIENT_ID); + + esp_err_t ret = esp_mqtt_client_start(s_mqtt_client); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to start MQTT client"); + s_mqtt_state = MQTT_STATE_ERROR; + return ret; + } + + s_mqtt_state = MQTT_STATE_CONNECTING; + ESP_LOGI(TAG, "MQTT client started"); + return ESP_OK; +} + +esp_err_t mqtt_client_stop(void) +{ + if (s_mqtt_client == NULL) { + return ESP_OK; + } + + ESP_LOGI(TAG, "Stopping MQTT client..."); + + // Publish offline status before stopping + if (s_mqtt_state == MQTT_STATE_CONNECTED) { + mqtt_publish_status(STATUS_OFFLINE); + vTaskDelay(100 / portTICK_PERIOD_MS); // Give time to send + } + + esp_err_t ret = esp_mqtt_client_stop(s_mqtt_client); + if (ret == ESP_OK) { + s_mqtt_state = MQTT_STATE_DISCONNECTED; + ESP_LOGI(TAG, "MQTT client stopped"); + } + + return ret; +} + +esp_err_t mqtt_client_publish(const char* topic, const char* data, int qos, int retain) +{ + if (s_mqtt_client == NULL || s_mqtt_state != MQTT_STATE_CONNECTED) { + ESP_LOGW(TAG, "MQTT client not connected"); + return ESP_ERR_INVALID_STATE; + } + + if (topic == NULL || data == NULL) { + return ESP_ERR_INVALID_ARG; + } + + xSemaphoreTake(s_mqtt_mutex, portMAX_DELAY); + + int msg_id = esp_mqtt_client_publish(s_mqtt_client, topic, data, strlen(data), qos, retain); + + xSemaphoreGive(s_mqtt_mutex); + + if (msg_id < 0) { + ESP_LOGE(TAG, "Failed to publish to topic: %s", topic); + return ESP_FAIL; + } + + ESP_LOGD(TAG, "Published to %s: %s (msg_id: %d)", topic, data, msg_id); + return ESP_OK; +} + +esp_err_t mqtt_client_subscribe(const char* topic, int qos) +{ + if (s_mqtt_client == NULL || s_mqtt_state != MQTT_STATE_CONNECTED) { + ESP_LOGW(TAG, "MQTT client not connected"); + return ESP_ERR_INVALID_STATE; + } + + if (topic == NULL) { + return ESP_ERR_INVALID_ARG; + } + + int msg_id = esp_mqtt_client_subscribe(s_mqtt_client, topic, qos); + if (msg_id < 0) { + ESP_LOGE(TAG, "Failed to subscribe to topic: %s", topic); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Subscribed to topic: %s (msg_id: %d)", topic, msg_id); + return ESP_OK; +} + +esp_err_t mqtt_client_unsubscribe(const char* topic) +{ + if (s_mqtt_client == NULL || s_mqtt_state != MQTT_STATE_CONNECTED) { + ESP_LOGW(TAG, "MQTT client not connected"); + return ESP_ERR_INVALID_STATE; + } + + if (topic == NULL) { + return ESP_ERR_INVALID_ARG; + } + + int msg_id = esp_mqtt_client_unsubscribe(s_mqtt_client, topic); + if (msg_id < 0) { + ESP_LOGE(TAG, "Failed to unsubscribe from topic: %s", topic); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Unsubscribed from topic: %s (msg_id: %d)", topic, msg_id); + return ESP_OK; +} + +bool mqtt_client_is_connected(void) +{ + return s_mqtt_state == MQTT_STATE_CONNECTED; +} + +mqtt_state_t mqtt_client_get_state(void) +{ + return s_mqtt_state; +} + +void mqtt_client_register_callbacks(mqtt_connected_callback_t on_connected, + mqtt_disconnected_callback_t on_disconnected, + mqtt_data_callback_t on_data) +{ + s_connected_callback = on_connected; + s_disconnected_callback = on_disconnected; + s_data_callback = on_data; +} + +// Utility functions +esp_err_t mqtt_publish_status(const char* status) +{ + return mqtt_client_publish(TOPIC_STATUS, status, MQTT_QOS_1, MQTT_RETAIN); +} + +esp_err_t mqtt_publish_moisture(int sensor_id, int value) +{ + char topic[64]; + char data[32]; + + snprintf(topic, sizeof(topic), "plant_watering/moisture/%d", sensor_id); + snprintf(data, sizeof(data), "%d", value); + + return mqtt_client_publish(topic, data, MQTT_QOS_0, MQTT_NO_RETAIN); +} + +esp_err_t mqtt_publish_pump_state(int pump_id, bool state) +{ + char topic[64]; + const char* state_str = state ? "on" : "off"; + + snprintf(topic, sizeof(topic), "plant_watering/pump/%d/state", pump_id); + + return mqtt_client_publish(topic, state_str, MQTT_QOS_1, MQTT_RETAIN); +} + +// Event handler +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%ld", base, event_id); + mqtt_event_handler_cb(event_data); +} + +static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event) +{ + switch (event->event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT connected"); + s_mqtt_state = MQTT_STATE_CONNECTED; + + // Publish online status + mqtt_publish_status(STATUS_ONLINE); + + // Subscribe to command topics + mqtt_client_subscribe(TOPIC_PUMP_1_CMD, MQTT_QOS_1); + mqtt_client_subscribe(TOPIC_PUMP_2_CMD, MQTT_QOS_1); + mqtt_client_subscribe(TOPIC_CONFIG, MQTT_QOS_1); + + // Call user callback + if (s_connected_callback) { + s_connected_callback(); + } + break; + + case MQTT_EVENT_DISCONNECTED: + ESP_LOGW(TAG, "MQTT disconnected"); + s_mqtt_state = MQTT_STATE_DISCONNECTED; + + // Call user callback + if (s_disconnected_callback) { + s_disconnected_callback(); + } + break; + + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGI(TAG, "Subscribed to topic, msg_id=%d", event->msg_id); + break; + + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGI(TAG, "Unsubscribed from topic, msg_id=%d", event->msg_id); + break; + + case MQTT_EVENT_PUBLISHED: + ESP_LOGD(TAG, "Message published, msg_id=%d", event->msg_id); + break; + + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT data received"); + ESP_LOGI(TAG, "Topic: %.*s", event->topic_len, event->topic); + ESP_LOGI(TAG, "Data: %.*s", event->data_len, event->data); + + // Call user callback + if (s_data_callback) { + // Null-terminate the strings for easier handling + char topic[256] = {0}; + char data[512] = {0}; + + int topic_len = event->topic_len < sizeof(topic) - 1 ? event->topic_len : sizeof(topic) - 1; + int data_len = event->data_len < sizeof(data) - 1 ? event->data_len : sizeof(data) - 1; + + memcpy(topic, event->topic, topic_len); + memcpy(data, event->data, data_len); + + s_data_callback(topic, data, data_len); + } + break; + + case MQTT_EVENT_ERROR: + ESP_LOGE(TAG, "MQTT error"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + ESP_LOGE(TAG, "Last error code reported from esp-tls: 0x%x", event->error_handle->esp_tls_last_esp_err); + ESP_LOGE(TAG, "Last tls stack error number: 0x%x", event->error_handle->esp_tls_stack_err); + ESP_LOGE(TAG, "Last captured errno : %d (%s)", event->error_handle->esp_transport_sock_errno, + strerror(event->error_handle->esp_transport_sock_errno)); + } else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { + ESP_LOGE(TAG, "Connection refused error: 0x%x", event->error_handle->connect_return_code); + } else { + ESP_LOGW(TAG, "Unknown error type: 0x%x", event->error_handle->error_type); + } + s_mqtt_state = MQTT_STATE_ERROR; + break; + + case MQTT_EVENT_BEFORE_CONNECT: + ESP_LOGI(TAG, "MQTT client connecting..."); + s_mqtt_state = MQTT_STATE_CONNECTING; + break; + + default: + ESP_LOGD(TAG, "Other MQTT event id: %d", event->event_id); + break; + } + + return ESP_OK; +} + +// Configuration management functions +esp_err_t mqtt_client_set_broker_url(const char* url) +{ + nvs_handle_t nvs_handle; + esp_err_t ret; + + ret = nvs_open(MQTT_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to open NVS namespace"); + return ret; + } + + ret = nvs_set_str(nvs_handle, "broker_url", url); + if (ret != ESP_OK) { + nvs_close(nvs_handle); + return ret; + } + + ret = nvs_commit(nvs_handle); + nvs_close(nvs_handle); + + if (ret == ESP_OK) { + ESP_LOGI(TAG, "MQTT broker URL saved to NVS"); + } + + return ret; +} + +esp_err_t mqtt_client_set_credentials(const char* username, const char* password) +{ + nvs_handle_t nvs_handle; + esp_err_t ret; + + ret = nvs_open(MQTT_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to open NVS namespace"); + return ret; + } + + ret = nvs_set_str(nvs_handle, "username", username); + if (ret != ESP_OK) { + nvs_close(nvs_handle); + return ret; + } + + ret = nvs_set_str(nvs_handle, "password", password); + if (ret != ESP_OK) { + nvs_close(nvs_handle); + return ret; + } + + ret = nvs_commit(nvs_handle); + nvs_close(nvs_handle); + + if (ret == ESP_OK) { + ESP_LOGI(TAG, "MQTT credentials saved to NVS"); + } + + return ret; +} + +esp_err_t mqtt_client_get_config(char* url, size_t url_len, + char* username, size_t username_len, + char* password, size_t password_len) +{ + nvs_handle_t nvs_handle; + esp_err_t ret; + + ret = nvs_open(MQTT_NVS_NAMESPACE, NVS_READONLY, &nvs_handle); + if (ret != ESP_OK) { + return ret; + } + + ret = nvs_get_str(nvs_handle, "broker_url", url, &url_len); + if (ret != ESP_OK) { + nvs_close(nvs_handle); + return ret; + } + + ret = nvs_get_str(nvs_handle, "username", username, &username_len); + if (ret != ESP_OK) { + nvs_close(nvs_handle); + return ret; + } + + ret = nvs_get_str(nvs_handle, "password", password, &password_len); + nvs_close(nvs_handle); + + return ret; +} \ No newline at end of file diff --git a/main/plant_mqtt.h b/main/plant_mqtt.h new file mode 100644 index 0000000..5126684 --- /dev/null +++ b/main/plant_mqtt.h @@ -0,0 +1,86 @@ +#ifndef PLANT_MQTT_H +#define PLANT_MQTT_H + +#include "esp_err.h" +#include + +// MQTT Configuration - These can be overridden by Kconfig +#ifndef CONFIG_MQTT_BROKER_URL +#define CONFIG_MQTT_BROKER_URL "mqtt://192.168.4.56:1883" +#endif + +#ifndef CONFIG_MQTT_USERNAME +#define CONFIG_MQTT_USERNAME "esp32" +#endif + +#ifndef CONFIG_MQTT_PASSWORD +#define CONFIG_MQTT_PASSWORD "esp32-plant" +#endif + +#ifndef MQTT_CLIENT_ID +#define MQTT_CLIENT_ID "plant_watering_esp32" +#endif + +#define MQTT_KEEPALIVE 60 +#define MQTT_QOS_0 0 +#define MQTT_QOS_1 1 +#define MQTT_RETAIN 1 +#define MQTT_NO_RETAIN 0 + +// MQTT Topics +#define TOPIC_STATUS "plant_watering/status" +#define TOPIC_MOISTURE_1 "plant_watering/moisture/1" +#define TOPIC_MOISTURE_2 "plant_watering/moisture/2" +#define TOPIC_PUMP_1_CMD "plant_watering/pump/1/set" +#define TOPIC_PUMP_2_CMD "plant_watering/pump/2/set" +#define TOPIC_PUMP_1_STATE "plant_watering/pump/1/state" +#define TOPIC_PUMP_2_STATE "plant_watering/pump/2/state" +#define TOPIC_CONFIG "plant_watering/config" +#define TOPIC_WATERING_STATS "plant_watering/stats" +#define TOPIC_LAST_WILL "plant_watering/status" + +// Status messages +#define STATUS_ONLINE "online" +#define STATUS_OFFLINE "offline" + +// MQTT States +typedef enum { + MQTT_STATE_DISCONNECTED, + MQTT_STATE_CONNECTING, + MQTT_STATE_CONNECTED, + MQTT_STATE_ERROR +} mqtt_state_t; + +// Callbacks +typedef void (*mqtt_connected_callback_t)(void); +typedef void (*mqtt_disconnected_callback_t)(void); +typedef void (*mqtt_data_callback_t)(const char* topic, const char* data, int data_len); + +// MQTT client functions +esp_err_t mqtt_client_init(void); +esp_err_t mqtt_client_start(void); +esp_err_t mqtt_client_stop(void); +esp_err_t mqtt_client_publish(const char* topic, const char* data, int qos, int retain); +esp_err_t mqtt_client_subscribe(const char* topic, int qos); +esp_err_t mqtt_client_unsubscribe(const char* topic); +bool mqtt_client_is_connected(void); +mqtt_state_t mqtt_client_get_state(void); + +// Callback registration +void mqtt_client_register_callbacks(mqtt_connected_callback_t on_connected, + mqtt_disconnected_callback_t on_disconnected, + mqtt_data_callback_t on_data); + +// Configuration management +esp_err_t mqtt_client_set_broker_url(const char* url); +esp_err_t mqtt_client_set_credentials(const char* username, const char* password); +esp_err_t mqtt_client_get_config(char* url, size_t url_len, + char* username, size_t username_len, + char* password, size_t password_len); + +// Utility functions for common publishes +esp_err_t mqtt_publish_status(const char* status); +esp_err_t mqtt_publish_moisture(int sensor_id, int value); +esp_err_t mqtt_publish_pump_state(int pump_id, bool state); + +#endif // PLANT_MQTT_H \ No newline at end of file