Added MQTT
This commit is contained in:
371
README.md
371
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
|
### ✅ Completed Features
|
||||||
- 🔄 **OTA Updates** - Web-based firmware updates with drag-and-drop interface
|
- **WiFi Manager**: Auto-connect with NVS credential storage
|
||||||
- 💡 **RGB LED Control** - WS2812 driver for visual feedback (GPIO 46)
|
- **OTA Updates**: Web-based firmware updates via HTTP server
|
||||||
- 🔧 **Docker-based Development** - No local ESP-IDF installation required
|
- **MQTT Client**: Full MQTT integration with NVS credential storage
|
||||||
- 📦 **Modular Design** - Easy to extend with additional features
|
- 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)
|
### 📋 TODO
|
||||||
- USB-C cable (data capable, not charge-only)
|
- Web dashboard
|
||||||
- 2.4GHz WiFi network
|
- 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)
|
||||||
```
|
```
|
||||||
.
|
CONFIG_WIFI_SSID="Your_SSID"
|
||||||
├── main/
|
CONFIG_WIFI_PASSWORD="Your_Password"
|
||||||
│ ├── 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
## Building and Flashing
|
||||||
- Git for version control
|
|
||||||
- Terminal/command line access
|
|
||||||
|
|
||||||
### 2. Clone and Configure
|
### Using Docker (Recommended)
|
||||||
|
|
||||||
|
#### Configure the project
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository (or create new project)
|
docker run --user $(id -u):$(id -g) --rm -v $PWD:/project -w /project -it espressif/idf:latest idf.py menuconfig
|
||||||
git clone <your-repo-url>
|
|
||||||
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 <project-name>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Build the Project
|
#### Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --user $(id -u):$(id -g) --rm -v $PWD:/project -w /project -it espressif/idf:latest idf.py build
|
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
|
```bash
|
||||||
docker run --privileged --rm -v $PWD:/project -w /project --device=/dev/ttyACM0 -it espressif/idf:latest idf.py flash -p /dev/ttyACM0
|
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.
|
#### Monitor serial output
|
||||||
|
|
||||||
### 5. Monitor Serial Output
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --privileged --rm -v $PWD:/project -w /project --device=/dev/ttyACM0 -it espressif/idf:latest idf.py monitor -p /dev/ttyACM0
|
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.
|
#### Flash and monitor in one command
|
||||||
|
```bash
|
||||||
## Using OTA Updates
|
docker run --privileged --rm -v $PWD:/project -w /project --device=/dev/ttyACM0 -it espressif/idf:latest idf.py flash monitor -p /dev/ttyACM0
|
||||||
|
|
||||||
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://<device-ip>/` in your browser
|
|
||||||
4. **Upload Firmware**:
|
|
||||||
- Build new version: Update `APP_VERSION` in main.c
|
|
||||||
- Run build command again
|
|
||||||
- Upload `build/<project-name>.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"},
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
```bash
|
||||||
// Set new credentials
|
# Configure the project
|
||||||
wifi_manager_set_credentials("NewSSID", "NewPassword");
|
idf.py menuconfig
|
||||||
|
|
||||||
// Check connection status
|
# Build
|
||||||
if (wifi_manager_is_connected()) {
|
idf.py build
|
||||||
// Connected
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear stored credentials
|
# Flash via USB
|
||||||
wifi_manager_clear_credentials();
|
idf.py -p /dev/ttyUSB0 flash monitor
|
||||||
```
|
```
|
||||||
|
|
||||||
### OTA Server
|
### OTA Updates
|
||||||
|
1. Connect to the same network as the ESP32
|
||||||
|
2. Navigate to `http://<ESP32_IP>/`
|
||||||
|
3. Upload the `build/PlantWater.bin` file
|
||||||
|
4. Device will automatically restart with new firmware
|
||||||
|
|
||||||
```c
|
## Testing with MQTT
|
||||||
// Set version string
|
|
||||||
ota_server_set_version("2.0.0");
|
|
||||||
|
|
||||||
// Register progress callback
|
### Monitor All Topics
|
||||||
ota_server_register_progress_callback(my_progress_handler);
|
```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
|
# Turn Pump 1 OFF
|
||||||
// Set LED color
|
docker run -it --rm --network mqtt-broker_mqtt-network eclipse-mosquitto:2.0.22 \
|
||||||
led_strip_set_pixel(strip, 0, 255, 0, 0); // Red
|
mosquitto_pub -h mosquitto -u home-server -P password -t "plant_watering/pump/1/set" -m "off"
|
||||||
led_strip_refresh(strip);
|
|
||||||
|
|
||||||
// Turn off
|
# Turn Pump 2 ON
|
||||||
led_strip_clear(strip);
|
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
|
## Troubleshooting
|
||||||
|
|
||||||
### Build Issues
|
### MQTT Connection Issues
|
||||||
- Ensure Docker is running and you have internet connection
|
- Check broker is running: `docker ps`
|
||||||
- Clean build: `idf.py fullclean` before building
|
- Verify credentials match broker configuration
|
||||||
|
- Ensure ESP32 and broker are on same network
|
||||||
### Flash Issues
|
- Check firewall rules for port 1883
|
||||||
- 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`
|
|
||||||
|
|
||||||
### WiFi Connection Issues
|
### WiFi Connection Issues
|
||||||
- Verify 2.4GHz network (ESP32 doesn't support 5GHz)
|
- Verify SSID has no trailing spaces
|
||||||
- Check for special characters in SSID/password
|
- Check password is correct
|
||||||
- Look for trailing spaces in SSID
|
- Ensure 2.4GHz network (ESP32 doesn't support 5GHz)
|
||||||
- Monitor serial output for specific error codes
|
- Try erasing flash and reflashing
|
||||||
|
|
||||||
### OTA Issues
|
### OTA Update Issues
|
||||||
- Ensure device has sufficient free space (check web interface)
|
- Ensure device is connected to network
|
||||||
- Verify binary file size fits in OTA partition (1.25MB max)
|
- Check partition table has OTA partitions
|
||||||
- Check same network connectivity between computer and ESP32
|
- Verify firmware size fits in OTA partition
|
||||||
|
- Try accessing `http://<ESP32_IP>/test` to verify server
|
||||||
|
|
||||||
## Memory Layout
|
## Next Development Steps
|
||||||
|
|
||||||
| Partition | Type | Size | Purpose |
|
1. **Motor Control Module**
|
||||||
|-----------|---------|---------|-------------------|
|
- PWM speed control
|
||||||
| nvs | data | 16KB | WiFi credentials |
|
- Safety timeouts
|
||||||
| otadata | data | 8KB | OTA selection |
|
- Current monitoring
|
||||||
| 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 |
|
|
||||||
|
|
||||||
## Security Considerations
|
2. **Moisture Sensor Module**
|
||||||
|
- ADC calibration
|
||||||
|
- Averaging/filtering
|
||||||
|
- Percentage conversion
|
||||||
|
|
||||||
For production deployments:
|
3. **Automation Logic**
|
||||||
- Add authentication to OTA web interface
|
- Threshold-based watering
|
||||||
- Use HTTPS for OTA updates
|
- Time-based schedules
|
||||||
- Implement firmware signature verification
|
- Prevent overwatering
|
||||||
- Store WiFi credentials securely
|
|
||||||
- Consider encrypted flash storage
|
|
||||||
|
|
||||||
## Extending the Template
|
4. **Enhanced Features**
|
||||||
|
- Web dashboard
|
||||||
This template provides core functionality. Add your application-specific features:
|
- Historical data logging
|
||||||
|
- Multi-zone support
|
||||||
1. **Remove LED code** if not using RGB LED
|
- Weather API integration
|
||||||
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/
|
|
||||||
@ -3,6 +3,7 @@ idf_component_register(
|
|||||||
"main.c"
|
"main.c"
|
||||||
"wifi_manager.c"
|
"wifi_manager.c"
|
||||||
"ota_server.c"
|
"ota_server.c"
|
||||||
|
"plant_mqtt.c"
|
||||||
"led_strip.c"
|
"led_strip.c"
|
||||||
INCLUDE_DIRS
|
INCLUDE_DIRS
|
||||||
"."
|
"."
|
||||||
@ -12,4 +13,5 @@ idf_component_register(
|
|||||||
esp_http_server
|
esp_http_server
|
||||||
app_update
|
app_update
|
||||||
driver
|
driver
|
||||||
|
mqtt
|
||||||
)
|
)
|
||||||
210
main/main.c
210
main/main.c
@ -5,54 +5,89 @@
|
|||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_chip_info.h"
|
#include "esp_chip_info.h"
|
||||||
|
#include "esp_random.h"
|
||||||
#include "wifi_manager.h"
|
#include "wifi_manager.h"
|
||||||
#include "ota_server.h"
|
#include "ota_server.h"
|
||||||
#include "led_strip.h"
|
#include "plant_mqtt.h"
|
||||||
|
|
||||||
#include "sdkconfig.h"
|
#include "sdkconfig.h"
|
||||||
|
|
||||||
static const char *TAG = "MAIN";
|
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
|
// Application version
|
||||||
#define APP_VERSION "1.0.1"
|
#define APP_VERSION "2.0.0-mqtt"
|
||||||
|
|
||||||
// LED colors and timing
|
// Test data
|
||||||
typedef struct {
|
static int test_moisture_1 = 45;
|
||||||
uint8_t r, g, b;
|
static int test_moisture_2 = 62;
|
||||||
const char *name;
|
static bool test_pump_1 = false;
|
||||||
} color_t;
|
static bool test_pump_2 = false;
|
||||||
|
|
||||||
static const color_t colors[] = {
|
// MQTT Callbacks
|
||||||
{255, 0, 0, "Red"},
|
static void mqtt_connected_callback(void)
|
||||||
{0, 255, 0, "Green"},
|
{
|
||||||
{0, 0, 255, "Blue"},
|
ESP_LOGI(TAG, "MQTT Connected - Publishing initial status");
|
||||||
{255, 255, 0, "Yellow"},
|
|
||||||
{255, 0, 255, "Magenta"},
|
// Publish initial states
|
||||||
{0, 255, 255, "Cyan"},
|
mqtt_publish_moisture(1, test_moisture_1);
|
||||||
{255, 255, 255, "White"},
|
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]))
|
static void mqtt_disconnected_callback(void)
|
||||||
#define BLINK_DELAY_MS 200
|
{
|
||||||
|
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
|
// WiFi event handler
|
||||||
static void wifi_event_handler(wifi_state_t state)
|
static void wifi_event_handler(wifi_state_t state)
|
||||||
{
|
{
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case WIFI_STATE_CONNECTED:
|
case WIFI_STATE_CONNECTED:
|
||||||
ESP_LOGI(TAG, "WiFi connected - starting OTA server");
|
ESP_LOGI(TAG, "WiFi connected - starting services");
|
||||||
ota_server_start();
|
ota_server_start();
|
||||||
|
|
||||||
|
// Start MQTT client
|
||||||
|
if (mqtt_client_start() != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to start MQTT client");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WIFI_STATE_DISCONNECTED:
|
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();
|
ota_server_stop();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -71,6 +106,35 @@ 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
|
||||||
|
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)
|
void print_chip_info(void)
|
||||||
{
|
{
|
||||||
esp_chip_info_t chip_info;
|
esp_chip_info_t chip_info;
|
||||||
@ -83,55 +147,38 @@ void print_chip_info(void)
|
|||||||
(chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "");
|
(chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "");
|
||||||
|
|
||||||
ESP_LOGI(TAG, "silicon revision %d, ", chip_info.revision);
|
ESP_LOGI(TAG, "silicon revision %d, ", chip_info.revision);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Minimum free heap size: %d bytes", esp_get_minimum_free_heap_size());
|
ESP_LOGI(TAG, "Minimum free heap size: %d bytes", esp_get_minimum_free_heap_size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void app_main(void)
|
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 information
|
||||||
print_chip_info();
|
print_chip_info();
|
||||||
|
|
||||||
// Initialize RGB LED
|
// Print MQTT configuration
|
||||||
led_strip_t *led_strip = led_strip_init(LED_STRIP_GPIO, LED_STRIP_LED_COUNT);
|
ESP_LOGI(TAG, "MQTT Broker: %s", CONFIG_MQTT_BROKER_URL);
|
||||||
if (!led_strip) {
|
ESP_LOGI(TAG, "MQTT Username: %s", CONFIG_MQTT_USERNAME);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
||||||
ota_server_register_progress_callback(ota_progress_handler);
|
ota_server_register_progress_callback(ota_progress_handler);
|
||||||
|
|
||||||
// Check if we have stored WiFi credentials
|
// Initialize MQTT client
|
||||||
char stored_ssid[33] = {0};
|
ESP_ERROR_CHECK(mqtt_client_init());
|
||||||
char stored_pass[65] = {0};
|
mqtt_client_register_callbacks(mqtt_connected_callback,
|
||||||
|
mqtt_disconnected_callback,
|
||||||
// Force update with new credentials (remove this after first successful connection)
|
mqtt_data_callback);
|
||||||
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);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Start WiFi connection
|
// Start WiFi connection
|
||||||
esp_err_t ret = wifi_manager_start();
|
esp_err_t ret = wifi_manager_start();
|
||||||
@ -139,42 +186,23 @@ void app_main(void)
|
|||||||
ESP_LOGE(TAG, "Failed to start WiFi manager");
|
ESP_LOGE(TAG, "Failed to start WiFi manager");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main loop with RGB LED blinking
|
// Create sensor simulation task
|
||||||
int color_index = 0;
|
xTaskCreate(sensor_simulation_task, "sensor_sim", 4096, NULL, 5, NULL);
|
||||||
bool led_on = false;
|
|
||||||
|
|
||||||
|
// Main loop - monitor system status
|
||||||
while (1) {
|
while (1) {
|
||||||
// Blink the RGB LED through different colors
|
ESP_LOGI(TAG, "System Status - WiFi: %s, MQTT: %s, Free heap: %d bytes",
|
||||||
if (led_strip) {
|
wifi_manager_is_connected() ? "Connected" : "Disconnected",
|
||||||
if (led_on) {
|
mqtt_client_is_connected() ? "Connected" : "Disconnected",
|
||||||
// Turn LED on with current color
|
esp_get_free_heap_size());
|
||||||
led_strip_set_pixel(led_strip, 0,
|
|
||||||
colors[color_index].r,
|
// Print pump states
|
||||||
colors[color_index].g,
|
if (mqtt_client_is_connected()) {
|
||||||
colors[color_index].b);
|
ESP_LOGI(TAG, "Pump States - Pump1: %s, Pump2: %s",
|
||||||
led_strip_refresh(led_strip);
|
test_pump_1 ? "ON" : "OFF",
|
||||||
ESP_LOGI(TAG, "LED ON - Color: %s", colors[color_index].name);
|
test_pump_2 ? "ON" : "OFF");
|
||||||
} 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print heap info every 10 blinks (5 seconds)
|
vTaskDelay(30000 / portTICK_PERIOD_MS); // Every 30 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
457
main/plant_mqtt.c
Normal file
457
main/plant_mqtt.c
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
86
main/plant_mqtt.h
Normal file
86
main/plant_mqtt.h
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#ifndef PLANT_MQTT_H
|
||||||
|
#define PLANT_MQTT_H
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
// 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
|
||||||
Reference in New Issue
Block a user