#include "./plant_water.h" #include "sht3x.h" #include "cjson.h" #define I2C_BUS 0 #define I2C_SCL_PIN 22 #define I2C_SDA_PIN 21 #define HTTPD_RESP_SIZE 100 #define MAX_CRON_SPECS 5 static const char *TAG = "pump"; static sht3x_sensor_t* sensor; static void set_pump(int pump_num, int state) { // Set duty to 100% ESP_ERROR_CHECK(ledc_set_duty(LEDC_MODE, pump_num, state == 1 ? LEDC_DUTY: 0)); // Update duty to apply the new value ESP_ERROR_CHECK(ledc_update_duty(LEDC_MODE, pump_num)); } static void pump_one_on() { set_pump(0, 1); set_pump(1, 0); } static void pump_two_on() { set_pump(0, 0); set_pump(1, 1); } static void pumps_off() { set_pump(0, 0); set_pump(1, 0); } typedef enum { PUMP_ON = 1, PUMP_OFF = 2, } event_type; struct event_t { event_type pump_1; event_type pump_2; int pump_delay; }; struct cron_t { int pump_num; int state; int pump_on_time; int hour; int minute; int last_ran_day; // day of the month it last ran int last_ran_minute; // minute it last ran int last_ran_hour; // hour it last ran }; static struct cron_t cron_specs[MAX_CRON_SPECS]; static TickType_t make_delay(int seconds) { return (1000*seconds) / portTICK_PERIOD_MS; } // Timer type definitions const TickType_t PUMP_TIMER_DELAY = (1000*60) / portTICK_PERIOD_MS; const TickType_t PUMP_CB_PERIOD = (1000*10) / portTICK_PERIOD_MS; TimerHandle_t pumpTimer = 0; // Timer for toggling the pumps on/off StaticTimer_t pumpTimerBuffer; // Memory backing for the timer, allocated statically // Queue type definitions uint8_t queueStorage[PUMP_EV_NUM*sizeof (struct event_t)]; // byte array for queue memory static StaticQueue_t pumpEvents; QueueHandle_t pumpEventsHandle; uint8_t timerQueueStorage[PUMP_EV_NUM*sizeof (struct cron_t)]; // byte array for queue memory static StaticQueue_t timerEvents; QueueHandle_t timerEventsHandle; // Task type definitions StaticTask_t xTaskBuffer; StackType_t xStack[TASK_STACK_SIZE]; void pumpRunnerTaskFunction(void *params) { struct event_t pumpMessage; printf("Task started\n"); configASSERT( ( uint32_t ) params == 1UL ); while (1) { vTaskDelay(2000 / portTICK_PERIOD_MS); if (pumpEventsHandle != NULL) { // The queue exists and is created if (xQueueReceive(pumpEventsHandle, &pumpMessage, (TickType_t)PUMP_TIMER_DELAY) == pdPASS) { printf("got a message, pump_1 = %u, pump_2 = %u, PUMP_ON = %u, PUMP_OFF = %u\n", pumpMessage.pump_1, pumpMessage.pump_2, PUMP_ON, PUMP_OFF); if (pumpMessage.pump_1 == PUMP_ON) { pump_one_on(); } if (pumpMessage.pump_2 == PUMP_ON) { pump_two_on(); } vTaskDelay(pumpMessage.pump_delay); pumps_off(); } } } } static void createPumpRunnerTask(void) { xTaskCreateStatic(pumpRunnerTaskFunction, "pumpt", TASK_STACK_SIZE, (void*)1, tskIDLE_PRIORITY + 2, xStack, &xTaskBuffer); } void runPumps(int delay1, int delay2) { struct event_t message; message.pump_1 = PUMP_ON; message.pump_2 = PUMP_OFF; message.pump_delay = make_delay(delay1); xQueueSend(pumpEventsHandle, (void*)&message, (TickType_t)0); message.pump_2 = PUMP_ON; message.pump_1 = PUMP_OFF; message.pump_delay = make_delay(delay2); xQueueSend(pumpEventsHandle, (void*)&message, (TickType_t)0); } void pumpTimerCb(TimerHandle_t pumpTimer) { time_t now; struct tm timeinfo; time(&now); localtime_r(&now, &timeinfo); struct cron_t pump_timer_config; static int next_cron_spec = 0; if (timerEventsHandle != NULL) { if (xQueueReceive(timerEventsHandle, &pump_timer_config, (TickType_t)0) == pdPASS) { printf("Got a new cron spec\n"); cron_specs[next_cron_spec] = pump_timer_config; next_cron_spec = (next_cron_spec + 1) % MAX_CRON_SPECS; printf("new: hour = %d, minute = %d\n", pump_timer_config.hour, pump_timer_config.minute); } } // check current number of cron specs, remove oldest one and replace if none left // loop over them and check time and execute commands for (int i = 0; i < MAX_CRON_SPECS; i++) { //printf("to match: hour = %d, minute = %d\n", cron_specs[i].hour, cron_specs[i].minute); //printf("current: hour = %d, minute = %d\n", timeinfo.tm_hour, timeinfo.tm_min); if (timeinfo.tm_hour == cron_specs[i].hour && (timeinfo.tm_min == cron_specs[i].minute)) { if (cron_specs[i].last_ran_day == timeinfo.tm_mday) { // it already ran today, skip it continue; } printf("running, hour = %d, minute = %d\n", cron_specs[i].hour, cron_specs[i].minute); // refactor this bit... struct event_t message = {0}; switch (cron_specs[i].pump_num) { case 0: message.pump_1 = PUMP_ON; message.pump_2 = PUMP_OFF; break; case 1: message.pump_2 = PUMP_ON; message.pump_1 = PUMP_OFF; break; } message.pump_delay = make_delay(cron_specs[i].pump_on_time); xQueueSend(pumpEventsHandle, (void*)&message, (TickType_t)0); cron_specs[i].last_ran_day = timeinfo.tm_mday; cron_specs[i].last_ran_hour = timeinfo.tm_hour; cron_specs[i].last_ran_minute = timeinfo.tm_min; } } } esp_err_t get_sensor_data(httpd_req_t *req) { time_t now; struct tm timeinfo; time(&now); localtime_r(&now, &timeinfo); float temperature; float humidity; char resp[HTTPD_RESP_SIZE] = {0}; cJSON *resp_object_j = cJSON_CreateObject(); if (sht3x_measure(sensor, &temperature, &humidity)) { cJSON_AddNumberToObject(resp_object_j, "temperature", (double)temperature); cJSON_AddNumberToObject(resp_object_j, "humidity", (double)humidity); } cJSON_AddNumberToObject(resp_object_j, "hour", (double)timeinfo.tm_hour); cJSON_AddNumberToObject(resp_object_j, "minute", (double)timeinfo.tm_min); cJSON_PrintPreallocated(resp_object_j, resp, HTTPD_RESP_SIZE, false); if (resp_object_j != NULL) { cJSON_Delete(resp_object_j); } httpd_resp_set_type(req, "application/json"); httpd_resp_set_status(req, HTTPD_200); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); return ESP_OK; } esp_err_t pumps_on_handler(httpd_req_t *req) { printf("pumps_on_handler executed\n"); // TODO stream char req_body[HTTPD_RESP_SIZE+1] = {0}; char resp[HTTPD_RESP_SIZE] = {0}; size_t body_size = MIN(req->content_len, (sizeof(req_body)-1)); // Receive body and do error handling int ret = httpd_req_recv(req, req_body, body_size); // if ret == 0 then no data if (ret < 0) { if (ret == HTTPD_SOCK_ERR_TIMEOUT) { httpd_resp_send_408(req); } return ESP_FAIL; } cJSON *json = cJSON_ParseWithLength(req_body, HTTPD_RESP_SIZE); cJSON *pump_one_time_j = NULL; cJSON *pump_two_time_j = NULL; if (json != NULL) { printf("%s\n", cJSON_Print(json)); if (cJSON_IsObject(json)) { pump_one_time_j = cJSON_GetObjectItemCaseSensitive(json, "pump_1"); pump_two_time_j = cJSON_GetObjectItemCaseSensitive(json, "pump_2"); if (cJSON_IsNumber(pump_one_time_j) && cJSON_IsNumber(pump_two_time_j)) { if (pump_one_time_j->valueint > 0 && pump_two_time_j->valueint > 0) { printf("Running pumps: p1 = %d, p2 = %d\n", pump_one_time_j->valueint, pump_two_time_j->valueint); runPumps(pump_one_time_j->valueint, pump_two_time_j->valueint); } } } } httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); if (json != NULL) { cJSON_Delete(json); } return ESP_OK; } /* URI handler structure for GET /uri */ httpd_uri_t uri_get = { .uri = "/sensor", .method = HTTP_GET, .handler = get_sensor_data, .user_ctx = NULL }; /* URI handler structure for POST /pumps_on */ httpd_uri_t pumps_on = { .uri = "/pumps_on", .method = HTTP_POST, .handler = pumps_on_handler, .user_ctx = NULL }; esp_err_t add_cron_handler(httpd_req_t *req) { printf("add_cron_handler executed\n"); // TODO stream char req_body[HTTPD_RESP_SIZE+1] = {0}; char resp[HTTPD_RESP_SIZE] = {0}; size_t body_size = MIN(req->content_len, (sizeof(req_body)-1)); // Receive body and do error handling int ret = httpd_req_recv(req, req_body, body_size); // if ret == 0 then no data if (ret < 0) { if (ret == HTTPD_SOCK_ERR_TIMEOUT) { httpd_resp_send_408(req); } return ESP_FAIL; } cJSON *json = cJSON_ParseWithLength(req_body, HTTPD_RESP_SIZE); cJSON *pump_num; cJSON *state; cJSON *pump_on_time; // time it will be on for cJSON *hour; // hour to trigger in cJSON *minute; // minute on the hour to trigger on struct cron_t cron_spec = {0}; if (json != NULL) { printf("%s\n", cJSON_Print(json)); if (cJSON_IsObject(json)) { pump_num = cJSON_GetObjectItemCaseSensitive(json, "pump_num"); state = cJSON_GetObjectItemCaseSensitive(json, "state"); pump_on_time = cJSON_GetObjectItemCaseSensitive(json, "pump_on_time"); hour = cJSON_GetObjectItemCaseSensitive(json, "hour"); minute = cJSON_GetObjectItemCaseSensitive(json, "minute"); if (cJSON_IsNumber(pump_num) && cJSON_IsNumber(state) && cJSON_IsNumber(pump_on_time) && cJSON_IsNumber(hour) && cJSON_IsNumber(minute)) { printf("Creating cron spec\n"); cron_spec.pump_num = pump_num->valueint; cron_spec.state = state->valueint; cron_spec.pump_on_time = pump_on_time->valueint; cron_spec.hour = hour->valueint; cron_spec.minute = minute->valueint; cron_spec.last_ran_day = -1; cron_spec.last_ran_hour = -1; cron_spec.last_ran_minute = -1; printf("Parsed: hour = %d, minute = %d\n", cron_spec.hour, cron_spec.minute); xQueueSend(timerEventsHandle, (void*)&cron_spec, (TickType_t)0); } } } httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); if (json != NULL) { cJSON_Delete(json); } return ESP_OK; } /* URI handler structure for POST /add_cron */ httpd_uri_t add_cron = { .uri = "/add_cron", .method = HTTP_POST, .handler = add_cron_handler, .user_ctx = NULL }; /* Function for starting the webserver */ httpd_handle_t start_webserver(void) { /* Generate default configuration */ httpd_config_t config = HTTPD_DEFAULT_CONFIG(); /* Empty handle to esp_http_server */ httpd_handle_t server = NULL; /* Start the httpd server */ if (httpd_start(&server, &config) == ESP_OK) { /* Register URI handlers */ httpd_register_uri_handler(server, &uri_get); httpd_register_uri_handler(server, &pumps_on); httpd_register_uri_handler(server, &add_cron); } /* If server failed to start, handle will be NULL */ ESP_LOGI(TAG, "webserver started"); return server; } static SemaphoreHandle_t s_semph_get_ip_addrs; /* tear down connection, release resources */ static void stop(void) { #if CONFIG_EXAMPLE_CONNECT_WIFI wifi_stop(); s_active_interfaces--; #endif } esp_err_t wifi_disconnect(void) { if (s_semph_get_ip_addrs == NULL) { return ESP_ERR_INVALID_STATE; } vSemaphoreDelete(s_semph_get_ip_addrs); s_semph_get_ip_addrs = NULL; stop(); ESP_ERROR_CHECK(esp_unregister_shutdown_handler(&stop)); return ESP_OK; } #ifndef INET6_ADDRSTRLEN #define INET6_ADDRSTRLEN 48 #endif /* Variable holding number of times ESP32 restarted since first boot. * It is placed into RTC memory using RTC_DATA_ATTR and * maintains its value when ESP32 wakes from deep sleep. */ RTC_DATA_ATTR static int boot_count = 0; static void obtain_time(void); static void initialize_sntp(void); void wifi_init_sta(void); void time_sync_notification_cb(struct timeval *tv) { ESP_LOGI(TAG, "Notification of a time synchronization event"); } static void obtain_time(void) { /** * NTP server address could be aquired via DHCP, * see following menuconfig options: * 'LWIP_DHCP_GET_NTP_SRV' - enable STNP over DHCP * 'LWIP_SNTP_DEBUG' - enable debugging messages * */ wifi_init_sta(); initialize_sntp(); // wait for time to be set time_t now = 0; struct tm timeinfo = { 0 }; int retry = 0; const int retry_count = 15; while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) { ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count); vTaskDelay(2000 / portTICK_PERIOD_MS); } time(&now); localtime_r(&now, &timeinfo);\ } static void initialize_sntp(void) { ESP_LOGI(TAG, "Initializing SNTP"); sntp_setoperatingmode(SNTP_OPMODE_POLL); /* * If 'NTP over DHCP' is enabled, we set dynamic pool address * as a 'secondary' server. It will act as a fallback server in case that address * provided via NTP over DHCP is not accessible */ #if LWIP_DHCP_GET_NTP_SRV && SNTP_MAX_SERVERS > 1 sntp_setservername(1, "pool.ntp.org"); #if LWIP_IPV6 && SNTP_MAX_SERVERS > 2 // statically assigned IPv6 address is also possible ip_addr_t ip6; if (ipaddr_aton("2a01:3f7::1", &ip6)) { // ipv6 ntp source "ntp.netnod.se" sntp_setserver(2, &ip6); } #endif /* LWIP_IPV6 */ #else /* LWIP_DHCP_GET_NTP_SRV && (SNTP_MAX_SERVERS > 1) */ // otherwise, use DNS address from a pool sntp_setservername(0, "pool.ntp.org"); #endif sntp_set_time_sync_notification_cb(time_sync_notification_cb); sntp_init(); ESP_LOGI(TAG, "List of configured NTP servers:"); for (uint8_t i = 0; i < SNTP_MAX_SERVERS; ++i){ if (sntp_getservername(i)){ ESP_LOGI(TAG, "server %d: %s", i, sntp_getservername(i)); } else { // we have either IPv4 or IPv6 address, let's print it char buff[INET6_ADDRSTRLEN]; ip_addr_t const *ip = sntp_getserver(i); if (ipaddr_ntoa_r(ip, buff, INET6_ADDRSTRLEN) != NULL) ESP_LOGI(TAG, "server %d: %s", i, buff); } } } /* FreeRTOS event group to signal when we are connected*/ static EventGroupHandle_t s_wifi_event_group; static int s_retry_num = 0; static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) { esp_wifi_connect(); s_retry_num++; ESP_LOGI(TAG, "retry to connect to the AP"); } else { xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } ESP_LOGI(TAG,"connect to the AP fail"); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); s_retry_num = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } } void wifi_init_sta(void) { s_wifi_event_group = xEventGroupCreate(); ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); #ifdef LWIP_DHCP_GET_NTP_SRV sntp_servermode_dhcp(1); // accept NTP offers from DHCP server, if any #endif esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); esp_event_handler_instance_t instance_any_id; esp_event_handler_instance_t instance_got_ip; ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id)); ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip)); wifi_config_t wifi_config = { .sta = { .ssid = EXAMPLE_ESP_WIFI_SSID, .password = EXAMPLE_ESP_WIFI_PASS, /* Setting a password implies station will connect to all security modes including WEP/WPA. * However these modes are deprecated and not advisable to be used. Incase your Access point * doesn't support WPA2, these mode can be enabled by commenting below line */ .threshold.authmode = ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD, }, }; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) ); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) ); ESP_ERROR_CHECK(esp_wifi_start() ); ESP_LOGI(TAG, "wifi_init_sta finished."); /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */ EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY); /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually * happened. */ if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS); httpd_handle_t server; printf("Trying to start webserver\n"); server = start_webserver(); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS); } else { ESP_LOGE(TAG, "UNEXPECTED EVENT"); } } static void ledc_init(int gpio_num, int ledc_channel_num, int ledc_timer_num) { // Prepare and then apply the LEDC PWM timer configuration ledc_timer_config_t ledc_timer = { .speed_mode = LEDC_MODE, .timer_num = ledc_timer_num, .duty_resolution = LEDC_DUTY_RES, .freq_hz = LEDC_FREQUENCY, // Set output frequency at 5 kHz .clk_cfg = LEDC_AUTO_CLK }; ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer)); // Prepare and then apply the LEDC PWM channel configuration ledc_channel_config_t ledc_channel = { .speed_mode = LEDC_MODE, .channel = ledc_channel_num, .timer_sel = ledc_timer_num, .intr_type = LEDC_INTR_DISABLE, .gpio_num = gpio_num, .duty = 0, // Set duty to 0% .hpoint = 0 }; ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel)); } void user_task (void *pvParameters) { float temperature; float humidity; TickType_t last_wakeup = xTaskGetTickCount(); while (1) { // perform one measurement and do something with the results if (sht3x_measure (sensor, &temperature, &humidity)) printf("%.3f SHT3x Sensor: %.2f °C, %.2f %%\n", (double)sdk_system_get_time()*1e-3, temperature, humidity); // wait until 5 seconds are over vTaskDelayUntil(&last_wakeup, 5000 / portTICK_PERIOD_MS); } } /* -- main program ------------------------------------------------- */ void user_init(void) { // Set UART Parameter. uart_set_baud(0, 115200); // Give the UART some time to settle vTaskDelay(1); // Init I2C bus interfaces at which SHT3x sensors are connected // (different busses are possible). i2c_init(I2C_BUS, I2C_SCL_PIN, I2C_SDA_PIN, I2C_FREQ_100K); // Create the sensors, multiple sensors are possible. if ((sensor = sht3x_init_sensor (I2C_BUS, SHT3x_ADDR_1))) { // Create a user task that uses the sensors. xTaskCreate(user_task, "user_task", TASK_STACK_SIZE, NULL, 2, 0); } // That's it. } void app_main(void) { ++boot_count; ESP_LOGI(TAG, "Boot count: %d", boot_count); time_t now; struct tm timeinfo; time(&now); localtime_r(&now, &timeinfo); // Is time set? If not, tm_year will be (1970 - 1900). if (timeinfo.tm_year < (2016 - 1900)) { ESP_LOGI(TAG, "Time is not set yet. Connecting to WiFi and getting time over NTP."); obtain_time(); // update 'now' variable with current time time(&now); } char strftime_buf[64]; // Set timezone to Eastern Standard Time and print local time setenv("TZ", "EST5EDT,M3.2.0/2,M11.1.0", 1); tzset(); localtime_r(&now, &timeinfo); strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo); ESP_LOGI(TAG, "The current date/time in New York is: %s", strftime_buf); if (sntp_get_sync_mode() == SNTP_SYNC_MODE_SMOOTH) { struct timeval outdelta; while (sntp_get_sync_status() == SNTP_SYNC_STATUS_IN_PROGRESS) { adjtime(NULL, &outdelta); ESP_LOGI(TAG, "Waiting for adjusting time ... outdelta = %jd sec: %li ms: %li us", (intmax_t)outdelta.tv_sec, outdelta.tv_usec/1000, outdelta.tv_usec%1000); vTaskDelay(2000 / portTICK_PERIOD_MS); } } pumpEventsHandle = xQueueCreateStatic(PUMP_EV_NUM, sizeof (struct event_t), queueStorage, &pumpEvents); timerEventsHandle = xQueueCreateStatic(PUMP_EV_NUM, sizeof (struct cron_t), timerQueueStorage, &timerEvents); configASSERT(pumpEventsHandle); configASSERT(timerEventsHandle); pumpTimer = xTimerCreateStatic("pump timer", PUMP_CB_PERIOD, pdTRUE, (void*)0, pumpTimerCb, &pumpTimerBuffer); xTimerStart(pumpTimer, 0); // Pump stuff // Set the LEDC peripheral configuration ledc_init(18, 0, 0); ledc_init(19, 1, 1); i2c_init(I2C_BUS, I2C_SCL_PIN, I2C_SDA_PIN, I2C_FREQ_100K); // Create the sensors, multiple sensors are possible. sensor = sht3x_init_sensor(I2C_BUS, SHT3x_ADDR_1); createPumpRunnerTask(); }