Added Scheduler

This commit is contained in:
2025-07-20 11:52:00 -06:00
parent 5a4c91fbd3
commit ba43d22a1a
5 changed files with 1679 additions and 32 deletions

View File

@ -10,6 +10,7 @@
#include "ota_server.h"
#include "plant_mqtt.h"
#include "motor_control.h"
#include "scheduler.h"
#include "sdkconfig.h"
// Uncomment this line to enable motor test mode with shorter intervals
@ -18,12 +19,15 @@
static const char *TAG = "MAIN";
// Application version
#define APP_VERSION "2.1.0-motor"
#define APP_VERSION "2.2.0-scheduler"
// Test data
static int test_moisture_1 = 45;
static int test_moisture_2 = 62;
// Function prototypes
static void print_chip_info(void);
// Motor Control Callbacks
static void motor_state_change_callback(motor_id_t id, motor_state_t state)
{
@ -63,6 +67,40 @@ static void motor_error_callback(motor_id_t id, const char* error)
}
}
// Scheduler callback
static void scheduler_trigger_callback(uint8_t pump_id, uint8_t schedule_id,
uint32_t duration_ms, uint8_t speed_percent)
{
ESP_LOGI(TAG, "Schedule %d triggered for pump %d: %lu ms at %d%%",
schedule_id, pump_id, duration_ms, speed_percent);
// Start the pump with the scheduled parameters
esp_err_t ret = motor_start_timed(pump_id, speed_percent, duration_ms);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start pump %d for schedule %d: %s",
pump_id, schedule_id, esp_err_to_name(ret));
// Publish error to MQTT
if (mqtt_client_is_connected()) {
char topic[64];
char msg[128];
snprintf(topic, sizeof(topic), "plant_watering/alerts/schedule_error/%d", pump_id);
snprintf(msg, sizeof(msg), "Schedule %d failed: %s", schedule_id, esp_err_to_name(ret));
mqtt_client_publish(topic, msg, MQTT_QOS_1, MQTT_NO_RETAIN);
}
} else {
// Publish schedule execution to MQTT
if (mqtt_client_is_connected()) {
char topic[64];
char msg[128];
snprintf(topic, sizeof(topic), "plant_watering/schedule/%d/executed", pump_id);
snprintf(msg, sizeof(msg), "{\"schedule_id\":%d,\"duration_ms\":%lu,\"speed\":%d}",
schedule_id, duration_ms, speed_percent);
mqtt_client_publish(topic, msg, MQTT_QOS_0, MQTT_NO_RETAIN);
}
}
}
// MQTT Callbacks
static void mqtt_connected_callback(void)
{
@ -80,6 +118,13 @@ static void mqtt_connected_callback(void)
"plant_watering/commands/test_pump/+",
"plant_watering/commands/emergency_stop",
"plant_watering/commands/test_mode",
"plant_watering/commands/holiday_mode",
"plant_watering/commands/get_time",
"plant_watering/commands/get_schedules",
"plant_watering/schedule/+/+/config",
"plant_watering/schedule/+/trigger",
"plant_watering/schedule/+/get",
"plant_watering/schedule/time/set",
"plant_watering/settings/+/+",
NULL
};
@ -196,13 +241,148 @@ static void mqtt_data_callback(const char* topic, const char* data, int data_len
motor_set_max_runtime(MOTOR_PUMP_1, CONFIG_WATERING_MAX_DURATION_MS);
motor_set_max_runtime(MOTOR_PUMP_2, CONFIG_WATERING_MAX_DURATION_MS);
}
} else if (strncmp(topic, "plant_watering/settings/pump/", 29) == 0) {
// Parse settings commands like:
// plant_watering/settings/pump/1/max_runtime
// plant_watering/settings/pump/1/min_interval
// plant_watering/settings/pump/1/min_speed
// plant_watering/settings/pump/1/max_speed
} else if (strcmp(topic, "plant_watering/commands/holiday_mode") == 0) {
if (strncmp(data, "on", data_len) == 0) {
scheduler_set_holiday_mode(true);
ESP_LOGI(TAG, "Holiday mode enabled - all schedules paused");
} else if (strncmp(data, "off", data_len) == 0) {
scheduler_set_holiday_mode(false);
ESP_LOGI(TAG, "Holiday mode disabled - schedules resumed");
}
} else if (strcmp(topic, "plant_watering/commands/get_time") == 0) {
// Publish current time information
if (scheduler_is_time_synchronized()) {
time_t now = scheduler_get_current_time();
struct tm timeinfo;
localtime_r(&now, &timeinfo);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &timeinfo);
char response[256];
snprintf(response, sizeof(response),
"{\"timestamp\":%lld,\"datetime\":\"%s\",\"timezone\":\"%s\",\"synced\":true}",
(long long)now, time_str, getenv("TZ") ? getenv("TZ") : "UTC");
mqtt_client_publish("plant_watering/system/time", response, MQTT_QOS_0, MQTT_NO_RETAIN);
ESP_LOGI(TAG, "Time: %s", time_str);
} else {
mqtt_client_publish("plant_watering/system/time",
"{\"synced\":false,\"message\":\"Time not synchronized\"}",
MQTT_QOS_0, MQTT_NO_RETAIN);
ESP_LOGW(TAG, "Time not synchronized");
}
} else if (strcmp(topic, "plant_watering/commands/get_schedules") == 0) {
// Publish all configured schedules
ESP_LOGI(TAG, "Publishing all schedules");
int active_count = 0;
// Publish each configured schedule
for (int pump = 1; pump <= 2; pump++) {
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
schedule_config_t config;
if (scheduler_get_schedule(pump, sched, &config) == ESP_OK) {
// Only publish if schedule is configured (not disabled)
if (config.type != SCHEDULE_TYPE_DISABLED) {
char topic_buf[64];
char json[512];
snprintf(topic_buf, sizeof(topic_buf),
"plant_watering/schedule/%d/%d/current", pump, sched);
if (scheduler_schedule_to_json(pump, sched, json, sizeof(json)) == ESP_OK) {
mqtt_client_publish(topic_buf, json, MQTT_QOS_0, MQTT_NO_RETAIN);
if (config.enabled) {
active_count++;
}
}
}
}
}
}
// Publish summary
char summary[256];
snprintf(summary, sizeof(summary),
"{\"total_schedules\":%d,\"active_schedules\":%d,\"holiday_mode\":%s,\"time_sync\":%s}",
active_count,
active_count,
scheduler_get_holiday_mode() ? "true" : "false",
scheduler_is_time_synchronized() ? "true" : "false");
mqtt_client_publish("plant_watering/schedule/summary", summary, MQTT_QOS_0, MQTT_NO_RETAIN);
ESP_LOGI(TAG, "Published %d schedules", active_count);
} else if (strncmp(topic, "plant_watering/schedule/", 24) == 0) {
// Parse schedule commands
if (strcmp(topic, "plant_watering/schedule/time/set") == 0) {
// Set system time manually (useful if no NTP)
time_t timestamp = atoll(data); // Use atoll for long long
if (timestamp > 0) {
scheduler_set_time(timestamp);
ESP_LOGI(TAG, "System time set to %lld", (long long)timestamp);
}
} else {
int pump_id = 0;
int schedule_id = 0;
char action[16] = {0};
int parsed = sscanf(topic + 24, "%d/%d/%15s", &pump_id, &schedule_id, action);
if (parsed == 2) {
// Check if it's a trigger command
if (sscanf(topic + 24, "%d/trigger", &pump_id) == 1) {
if (pump_id >= 1 && pump_id <= 2) {
// Trigger all enabled schedules for this pump
ESP_LOGI(TAG, "Manual trigger for pump %d schedules", pump_id);
for (int i = 0; i < SCHEDULER_MAX_SCHEDULES_PER_PUMP; i++) {
scheduler_trigger_schedule(pump_id, i);
}
}
}
// Check if it's a get command for specific pump
else if (sscanf(topic + 24, "%d/get", &pump_id) == 1) {
if (pump_id >= 1 && pump_id <= 2) {
ESP_LOGI(TAG, "Getting schedules for pump %d", pump_id);
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
schedule_config_t config;
if (scheduler_get_schedule(pump_id, sched, &config) == ESP_OK &&
config.type != SCHEDULE_TYPE_DISABLED) {
char topic_buf[64];
char json[512];
snprintf(topic_buf, sizeof(topic_buf),
"plant_watering/schedule/%d/%d/current", pump_id, sched);
if (scheduler_schedule_to_json(pump_id, sched, json, sizeof(json)) == ESP_OK) {
mqtt_client_publish(topic_buf, json, MQTT_QOS_0, MQTT_NO_RETAIN);
}
}
}
}
}
} else if (parsed == 3 && strcmp(action, "config") == 0) {
// Configure schedule
if (pump_id >= 1 && pump_id <= 2 &&
schedule_id >= 0 && schedule_id < SCHEDULER_MAX_SCHEDULES_PER_PUMP) {
esp_err_t ret = scheduler_json_to_schedule(data, pump_id, schedule_id);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Updated schedule %d for pump %d", schedule_id, pump_id);
// Publish confirmation
char response_topic[64];
char response[512];
snprintf(response_topic, sizeof(response_topic),
"plant_watering/schedule/%d/%d/status", pump_id, schedule_id);
scheduler_schedule_to_json(pump_id, schedule_id, response, sizeof(response));
mqtt_client_publish(response_topic, response, MQTT_QOS_0, MQTT_RETAIN);
} else {
ESP_LOGE(TAG, "Failed to update schedule: %s", esp_err_to_name(ret));
}
}
}
}
} else if (strncmp(topic, "plant_watering/settings/pump/", 29) == 0) {
// Parse settings commands
int pump_id = 0;
char setting[32] = {0};
@ -218,10 +398,7 @@ static void mqtt_data_callback(const char* topic, const char* data, int data_len
motor_set_min_interval(pump_id, value);
ESP_LOGI(TAG, "Set pump %d min interval to %d ms", pump_id, value);
} else if (strcmp(setting, "min_speed") == 0 && value >= 0 && value <= 100) {
// Get current max speed to validate
motor_stats_t stats;
motor_get_stats(pump_id, &stats);
motor_set_speed_limits(pump_id, value, 100); // Assuming max stays at 100
motor_set_speed_limits(pump_id, value, 100);
ESP_LOGI(TAG, "Set pump %d min speed to %d%%", pump_id, value);
} else if (strcmp(setting, "max_speed") == 0 && value > 0 && value <= 100) {
motor_set_speed_limits(pump_id, MOTOR_MIN_SPEED, value);
@ -321,34 +498,62 @@ static void sensor_simulation_task(void *pvParameters)
}
}
// Task to demonstrate automated watering based on moisture
static void automation_demo_task(void *pvParameters)
// Task to publish schedule status periodically
static void schedule_status_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
}
vTaskDelay(pdMS_TO_TICKS(60000)); // Every minute
if (mqtt_client_is_connected() && scheduler_is_time_synchronized()) {
// Publish scheduler status
scheduler_status_t status;
if (scheduler_get_status(&status) == ESP_OK) {
char status_json[256];
snprintf(status_json, sizeof(status_json),
"{\"holiday_mode\":%s,\"time_sync\":%s,\"active_schedules\":%lu,\"time\":%lld}",
status.holiday_mode ? "true" : "false",
status.time_synchronized ? "true" : "false",
status.active_schedules,
(long long)scheduler_get_current_time());
mqtt_client_publish("plant_watering/schedule/status", status_json, MQTT_QOS_0, MQTT_RETAIN);
}
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
// Publish human-readable time periodically
time_t now = scheduler_get_current_time();
struct tm timeinfo;
localtime_r(&now, &timeinfo);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &timeinfo);
char time_json[256];
snprintf(time_json, sizeof(time_json),
"{\"timestamp\":%lld,\"datetime\":\"%s\",\"day_of_week\":%d,\"hour\":%d,\"minute\":%d}",
(long long)now, time_str, timeinfo.tm_wday, timeinfo.tm_hour, timeinfo.tm_min);
mqtt_client_publish("plant_watering/system/current_time", time_json, MQTT_QOS_0, MQTT_NO_RETAIN);
// Publish all active schedules
for (int pump = 1; pump <= 2; pump++) {
for (int sched = 0; sched < SCHEDULER_MAX_SCHEDULES_PER_PUMP; sched++) {
schedule_config_t config;
if (scheduler_get_schedule(pump, sched, &config) == ESP_OK &&
config.enabled && config.type != SCHEDULE_TYPE_DISABLED) {
char topic[64];
char json[512];
snprintf(topic, sizeof(topic), "plant_watering/schedule/%d/%d/status", pump, sched);
if (scheduler_schedule_to_json(pump, sched, json, sizeof(json)) == ESP_OK) {
mqtt_client_publish(topic, json, MQTT_QOS_0, MQTT_RETAIN);
}
}
}
}
}
vTaskDelay(30000 / portTICK_PERIOD_MS); // Check every 30 seconds
}
}
void print_chip_info(void)
static void print_chip_info(void)
{
esp_chip_info_t chip_info;
@ -372,6 +577,7 @@ void app_main(void)
// Print configuration
ESP_LOGI(TAG, "Configuration:");
ESP_LOGI(TAG, " MQTT Broker: %s", CONFIG_MQTT_BROKER_URL);
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);
@ -413,6 +619,10 @@ void app_main(void)
motor_set_min_interval(MOTOR_PUMP_2, CONFIG_WATERING_MIN_INTERVAL_MS);
#endif
// Initialize Scheduler
ESP_ERROR_CHECK(scheduler_init());
scheduler_register_trigger_callback(scheduler_trigger_callback);
// Start WiFi connection
esp_err_t ret = wifi_manager_start();
if (ret != ESP_OK) {
@ -422,14 +632,15 @@ 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);
// Create schedule status task
xTaskCreate(schedule_status_task, "schedule_status", 4096, NULL, 4, NULL);
// Main loop - monitor system status
while (1) {
ESP_LOGI(TAG, "System Status - WiFi: %s, MQTT: %s, Free heap: %d bytes",
ESP_LOGI(TAG, "System Status - WiFi: %s, MQTT: %s, Time: %s, Free heap: %d bytes",
wifi_manager_is_connected() ? "Connected" : "Disconnected",
mqtt_client_is_connected() ? "Connected" : "Disconnected",
scheduler_is_time_synchronized() ? "Synced" : "Not synced",
esp_get_free_heap_size());
// Print pump states and runtime
@ -448,6 +659,24 @@ void app_main(void)
ESP_LOGI(TAG, "Pump %d: %s, Total runtime: %lu s, Runs: %lu",
i, state_str, stats.total_runtime_ms / 1000, stats.run_count);
}
// Print scheduler status
if (scheduler_is_time_synchronized()) {
scheduler_status_t sched_status;
if (scheduler_get_status(&sched_status) == ESP_OK) {
time_t now = scheduler_get_current_time();
struct tm timeinfo;
localtime_r(&now, &timeinfo);
char datetime_str[32];
strftime(datetime_str, sizeof(datetime_str), "%Y-%m-%d %H:%M:%S", &timeinfo);
ESP_LOGI(TAG, "Scheduler: %d active, Holiday: %s, DateTime: %s",
sched_status.active_schedules,
sched_status.holiday_mode ? "ON" : "OFF",
datetime_str);
}
}
}
vTaskDelay(30000 / portTICK_PERIOD_MS); // Every 30 seconds