commit 33ae8b72948b04d2706bf8c6c5702d1b041edd48 Author: Nicolas MASSE Date: Mon Jun 6 18:26:15 2022 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f80a6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build +sdkconfig +sdkconfig.old + +# Used during development to test OTA +# See https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/system.html +version.txt diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cd8fbb7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "esp32-owb"] + path = components/esp32-owb + url = https://github.com/DavidAntliff/esp32-owb.git + branch = master +[submodule "esp32-ds18b20"] + path = components/esp32-ds18b20 + url = https://github.com/DavidAntliff/esp32-ds18b20.git + branch = master diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b12da45 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(solar_controller) + +target_add_binary_data(solar_controller.elf "main/cacert.pem" TEXT) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1044519 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := solar_controller + +include $(IDF_PATH)/make/project.mk diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e64cad --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Solar Controller + diff --git a/components/esp32-ds18b20 b/components/esp32-ds18b20 new file mode 160000 index 0000000..d677f09 --- /dev/null +++ b/components/esp32-ds18b20 @@ -0,0 +1 @@ +Subproject commit d677f09a42898a7c3bd064cf8afd89f9d4d6ada9 diff --git a/components/esp32-owb b/components/esp32-owb new file mode 160000 index 0000000..60d977e --- /dev/null +++ b/components/esp32-owb @@ -0,0 +1 @@ +Subproject commit 60d977e7031f3fcb3900bce4f88befb159bfef4f diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..a935af1 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "main.c" "solar.c" "wifi.c" "mqtt.c" "sntp.c" "common.c" "ota.c" + INCLUDE_DIRS ".") diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..63c2fdc --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,111 @@ +menu "Solar controller configuration" + + config SC_1WIRE_GPIO + int "GPIO number for the 1-wire bus" + range 0 34 if IDF_TARGET_ESP32 + range 0 46 if IDF_TARGET_ESP32S2 + range 0 19 if IDF_TARGET_ESP32C3 + default 4 + help + GPIO number for the 1-wire bus. + + config SC_SAMPLE_PERIOD + int "Number of seconds between readings." + range 1 3600 + default 1 + help + Number of seconds between readings. + + config SC_PANEL_PUMP_GPIO + int "PIN used to send PWM signal to the panel pump." + range 0 34 if IDF_TARGET_ESP32 + range 0 46 if IDF_TARGET_ESP32S2 + range 0 19 if IDF_TARGET_ESP32C3 + default 19 + help + GPIO number for the panel pump. + + config SC_FLOOR_HEATING_PUMP_GPIO + int "PIN used to send PWM signal to the floor heating pump." + range 0 34 if IDF_TARGET_ESP32 + range 0 46 if IDF_TARGET_ESP32S2 + range 0 19 if IDF_TARGET_ESP32C3 + default 21 + help + GPIO number for the floor heating pump. + + config SC_PUMP_PWM_FREQ + int "PWM Frequency used to drive both pumps." + range 1000 10000 + default 1000 + help + PWM Frequency used to drive both pumps. + + config SC_STACK_SIZE + int "Stack size for the solar controller." + range 1024 16384 + default 4096 + help + Defines stack size. Insufficient stack size can cause crash. + + config SC_FROST_PROTECTION_TEMP_LOW + int "Frost protection temperature (lower bound)." + range -30 100 + default -4 + help + If the temperature is lower, starts the panel pump. + + config SC_FROST_PROTECTION_TEMP_HIGH + int "Frost protection temperature (higher bound)." + range -30 100 + default 1 + help + If the temperature is higher, stops the panel pump. + + config SC_PANEL_DELTA_TEMP_LOW + int "Difference of temperature at which the panel pump stops (lower bound)." + range -30 100 + default 5 + help + If the temperature is lower, starts the panel pump. + + config SC_PANEL_DELTA_TEMP_HIGH + int "Difference of temperature at which the panel pump starts (higher bound)." + range -30 100 + default 10 + help + If the temperature is higher, stops the panel pump. + + config SC_FLOOR_HEATING_DELTA_TEMP_LOW + int "Difference of temperature at which the floor heating pump stops (lower bound)." + range -30 100 + default 5 + help + If the temperature is lower, starts the panel pump. + + config SC_FLOOR_HEATING_DELTA_TEMP_HIGH + int "Difference of temperature at which the floor heating pump starts (higher bound)." + range -30 100 + default 7 + help + If the temperature is higher, stops the panel pump. + + config MQTT_LWT_TOPIC + string "MQTT Last Will Testament Topic" + default "solar-controller/connected" + help + Topic where to send LWT (Last Will Testament) upon disconnect. + + config MQTT_COMMAND_TOPIC + string "MQTT system command Topic" + default "solar-controller/command" + help + Topic where to send system commands. + + config MQTT_SOLAR_VALUE_TOPIC + string "Where to send measures" + default "solar-controller/status/solar/%s" + help + Topic where to measures. + +endmenu diff --git a/main/cacert.pem b/main/cacert.pem new file mode 100644 index 0000000..4d6f390 --- /dev/null +++ b/main/cacert.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE +BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD +EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG +EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT +DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r +Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 +3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K +b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN +Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ +4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf +1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu +hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH +usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r +OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY +9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV +0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt +hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw +TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx +e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA +JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD +YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n +JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ +m+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/main/common.c b/main/common.c new file mode 100644 index 0000000..5ac51a1 --- /dev/null +++ b/main/common.c @@ -0,0 +1,20 @@ +#include "common.h" +volatile EventGroupHandle_t services_event_group; + +char* get_nvs_string(nvs_handle_t nvs, char* key) { + size_t required_size; + esp_err_t err = nvs_get_str(nvs, key, NULL, &required_size); + if (err != ESP_OK) { + return NULL; + } + char* value = malloc(required_size); + if (!value) { + return NULL; + } + err = nvs_get_str(nvs, key, value, &required_size); + if (err != ESP_OK) { + free(value); + return NULL; + } + return value; +} diff --git a/main/common.h b/main/common.h new file mode 100644 index 0000000..90608bf --- /dev/null +++ b/main/common.h @@ -0,0 +1,20 @@ +#ifndef __COMMON_H__ +#define __COMMON_H__ + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "nvs_flash.h" +#include "nvs.h" + +#define WIFI_CONNECTED_BIT BIT0 +#define MQTT_CONNECTED_BIT BIT1 +#define TIME_SYNC_BIT BIT2 + +extern volatile EventGroupHandle_t services_event_group; +char* get_nvs_string(nvs_handle_t nvs, char* key); + +#define WAIT_FOR(flags) while ((xEventGroupWaitBits(services_event_group, flags, pdFALSE, pdTRUE, portMAX_DELAY) & (flags)) != (flags)) {} +#define IS_READY(flags) ((xEventGroupGetBits(services_event_group) & (flags)) == (flags)) + +#endif diff --git a/main/component.mk b/main/component.mk new file mode 100644 index 0000000..ef8ee82 --- /dev/null +++ b/main/component.mk @@ -0,0 +1,4 @@ +# +# Main Makefile. This is basically the same as a component makefile. +# +COMPONENT_EMBED_TXTFILES := cacert.pem diff --git a/main/main.c b/main/main.c new file mode 100644 index 0000000..bf15a19 --- /dev/null +++ b/main/main.c @@ -0,0 +1,48 @@ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "driver/uart.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "esp_tls.h" +#include "esp_crt_bundle.h" +#include "wifi.h" +#include "mqtt.h" +#include "solar.h" +#include "sntp.h" +#include "common.h" +#include "esp_ota_ops.h" +#include "ota.h" + +// Embedded via component.mk +extern const uint8_t cacert_pem_start[] asm("_binary_cacert_pem_start"); +extern const uint8_t cacert_pem_end[] asm("_binary_cacert_pem_end"); + +void app_main(void) { + const esp_app_desc_t* current = esp_ota_get_app_description(); + ESP_LOGI("main", "Currently running %s version %s", current->project_name, current->version); + + // NVS is used to store wifi credentials. So, we need to initialize it first. + ESP_ERROR_CHECK(nvs_flash_init()); + + // Inject the Let's Encrypt Root CA certificate in the global CA store + ESP_ERROR_CHECK(esp_tls_init_global_ca_store()); + ESP_ERROR_CHECK(esp_tls_set_global_ca_store((const unsigned char *) cacert_pem_start, cacert_pem_end - cacert_pem_start)); + + // Create an event group to keep track of service readiness + services_event_group = xEventGroupCreate(); + + // Start the solar controller early because we need it working even in case + // of missing wifi network. + solar_init(); + + // Then connect to Wifi, MQTT and NTP + wifi_init_sta(); + WAIT_FOR(WIFI_CONNECTED_BIT); + mqtt_init(); + sntp_start(); + WAIT_FOR(MQTT_CONNECTED_BIT|TIME_SYNC_BIT); +} diff --git a/main/mqtt.c b/main/mqtt.c new file mode 100644 index 0000000..d6afebe --- /dev/null +++ b/main/mqtt.c @@ -0,0 +1,242 @@ +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "nvs_flash.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_log.h" +#include "mqtt_client.h" +#include "esp_tls.h" +#include "esp_ota_ops.h" +#include +#include +#include "cJSON.h" +#include "common.h" +#include "ota.h" +#include "mqtt.h" +#include "solar.h" + +static esp_mqtt_client_config_t mqtt_cfg; +static esp_mqtt_client_handle_t client; + +static const char *MQTT_LOGGER = "mqtt"; + +#define MQTT_QOS_0 0 +#define MQTT_QOS_1 1 +#define MQTT_QOS_2 2 + +#define MQTT_NO_RETAIN 0 +#define MQTT_RETAIN 1 + +#define JSON_BUFFER_SIZE 128 +#define MQTT_TOPIC_BUFFER_SIZE 128 + +#define SYSTEM_COMMAND_UPDATE "firmware-update" +#define SYSTEM_COMMAND_REBOOT "reboot" +#define SYSTEM_COMMAND_SET_PARAM "set-parameter" +#define PARAMETER_HEATING_ENABLED "heating_enabled" + +void mqtt_publish_data(char* key, json_value jv) { + char topic[MQTT_TOPIC_BUFFER_SIZE]; + char payload[JSON_BUFFER_SIZE]; + time_t now; + int retain = MQTT_RETAIN; + int qos = MQTT_QOS_1; + + // Format the MQTT topic + if (!snprintf(topic, MQTT_TOPIC_BUFFER_SIZE, CONFIG_MQTT_SOLAR_VALUE_TOPIC, key)) { + ESP_LOGD(MQTT_LOGGER, "mqtt_publish_data: snprintf failed!"); + return; + } + + cJSON *root = cJSON_CreateObject(); + if (root == NULL) { + ESP_LOGD(MQTT_LOGGER, "mqtt_publish_data: cJSON_CreateObject failed!"); + return; + } + + // Add the value + if (jv.type == MQTT_TYPE_STRING) { + cJSON_AddStringToObject(root, "val", (char*)jv.value.str); + } else if (jv.type == MQTT_TYPE_FLOAT) { + float f = jv.value.f; + cJSON_AddNumberToObject(root, "val", (double)f); + } else if (jv.type == MQTT_TYPE_INT) { + int i = jv.value.i; + cJSON_AddNumberToObject(root, "val", (double)i); + } + + // Add a timestamp + time(&now); + cJSON_AddNumberToObject(root, "ts", (double)now); + + if (!cJSON_PrintPreallocated(root, payload, JSON_BUFFER_SIZE, 0)) { + ESP_LOGD(MQTT_LOGGER, "mqtt_publish_data: cJSON_PrintPreallocated failed!"); + cJSON_Delete(root); + return; + } + cJSON_Delete(root); + + if (esp_mqtt_client_publish(client, topic, payload, 0, qos, retain) == -1) { + ESP_LOGD(MQTT_LOGGER, "MQTT Message discarded!"); + } +} + +esp_err_t mqtt_process_system_command(char* data, int data_len) { + esp_err_t status = ESP_OK; + cJSON *json = cJSON_ParseWithLength(data, data_len); + if (json == NULL) { + ESP_LOGI(MQTT_LOGGER, "Error parsing MQTT system command as JSON"); + status = ESP_FAIL; + goto end; + } + + cJSON* command = cJSON_GetObjectItemCaseSensitive(json, "command"); + if (!cJSON_IsString(command) || (command->valuestring == NULL)) { + ESP_LOGD(MQTT_LOGGER, "Expected a command name!"); + status = ESP_FAIL; + goto end; + } + + if (strcmp(command->valuestring, SYSTEM_COMMAND_REBOOT) == 0) { + ESP_LOGE(MQTT_LOGGER, "Received a reboot command. Rebooting now!"); + esp_restart(); + goto end; + } + + if (strcmp(command->valuestring, SYSTEM_COMMAND_UPDATE) == 0) { + cJSON* args = cJSON_GetObjectItemCaseSensitive(json, "args"); + if (!cJSON_IsObject(args)) { + ESP_LOGD(MQTT_LOGGER, "Expected a command argument!"); + status = ESP_FAIL; + goto end; + } + + cJSON* version = cJSON_GetObjectItemCaseSensitive(args, "version"); + if (!cJSON_IsString(version) || (version->valuestring == NULL)) { + ESP_LOGD(MQTT_LOGGER, "Expected a version numer!"); + status = ESP_FAIL; + goto end; + } + + trigger_ota_update(version->valuestring); + goto end; + } else if (strcmp(command->valuestring, SYSTEM_COMMAND_SET_PARAM) == 0) { + cJSON* args = cJSON_GetObjectItemCaseSensitive(json, "args"); + if (!cJSON_IsObject(args)) { + ESP_LOGD(MQTT_LOGGER, "Expected a command argument!"); + status = ESP_FAIL; + goto end; + } + + cJSON* name = cJSON_GetObjectItemCaseSensitive(args, "name"); + if (!cJSON_IsString(name) || (name->valuestring == NULL)) { + ESP_LOGD(MQTT_LOGGER, "Expected a parameter name!"); + status = ESP_FAIL; + goto end; + } + + char * param_name = name->valuestring; + if (strcmp(param_name, PARAMETER_HEATING_ENABLED) == 0) { + cJSON* value = cJSON_GetObjectItemCaseSensitive(args, "value"); + if (!cJSON_IsNumber(value)) { + ESP_LOGD(MQTT_LOGGER, "Expected a parameter value with type 'number'!"); + status = ESP_FAIL; + goto end; + } + int heating_enabled = value->valuedouble; + solar_set_heating(heating_enabled); + } + + goto end; + } + + ESP_LOGW(MQTT_LOGGER, "Unknown system command %s!", command->valuestring); + status = ESP_FAIL; + + end: + cJSON_Delete(json); + return status; +} + +esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event) { + switch (event->event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(MQTT_LOGGER, "MQTT_EVENT_CONNECTED"); + xEventGroupSetBits(services_event_group, MQTT_CONNECTED_BIT); + if (esp_mqtt_client_publish(client, CONFIG_MQTT_LWT_TOPIC, "1", 0, MQTT_QOS_0, MQTT_RETAIN) == -1) { + ESP_LOGD(MQTT_LOGGER, "MQTT Message discarded!"); + } + if (esp_mqtt_client_subscribe(client, CONFIG_MQTT_COMMAND_TOPIC, MQTT_QOS_1) == -1) { + ESP_LOGD(MQTT_LOGGER, "Could not subscribe to " CONFIG_MQTT_COMMAND_TOPIC " MQTT topic"); + } + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(MQTT_LOGGER, "MQTT_EVENT_DISCONNECTED"); + xEventGroupClearBits(services_event_group, MQTT_CONNECTED_BIT); + break; + case MQTT_EVENT_PUBLISHED: + ESP_LOGD(MQTT_LOGGER, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_ERROR: + ESP_LOGI(MQTT_LOGGER, "MQTT_EVENT_ERROR"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + ESP_LOGI(MQTT_LOGGER, "Last error code reported from esp-tls: 0x%x", event->error_handle->esp_tls_last_esp_err); + ESP_LOGI(MQTT_LOGGER, "Last tls stack error number: 0x%x", event->error_handle->esp_tls_stack_err); + ESP_LOGI(MQTT_LOGGER, "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_LOGI(MQTT_LOGGER, "Connection refused error: 0x%x", event->error_handle->connect_return_code); + } else { + ESP_LOGW(MQTT_LOGGER, "Unknown error type: 0x%x", event->error_handle->error_type); + } + break; + case MQTT_EVENT_DATA: + if (strncmp(event->topic, CONFIG_MQTT_COMMAND_TOPIC, event->topic_len) == 0) { + ESP_LOGD(MQTT_LOGGER, "Received an MQTT system command!"); + mqtt_process_system_command(event->data, event->data_len); + } + break; + case MQTT_EVENT_SUBSCRIBED: + // Expected event. Nothing to do. + break; + default: + ESP_LOGD(MQTT_LOGGER, "Other event id:%d", event->event_id); + break; + } + return ESP_OK; +} + +void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { + ESP_LOGD(MQTT_LOGGER, "Event dispatched from event loop base=%s, event_id=%d", base, event_id); + mqtt_event_handler_cb(event_data); +} + +void mqtt_init(void) { + nvs_handle_t nvs; + esp_err_t err = nvs_open("mqtt", NVS_READONLY, &nvs); + if (err != ESP_OK) { + ESP_LOGE(MQTT_LOGGER, "Error (%s) opening NVS handle!\n", esp_err_to_name(err)); + return; + } + + memset(&mqtt_cfg, 0, sizeof(mqtt_cfg)); + mqtt_cfg.uri = get_nvs_string(nvs, "url"); + mqtt_cfg.use_global_ca_store = true; + mqtt_cfg.username = get_nvs_string(nvs, "username"); + mqtt_cfg.password = get_nvs_string(nvs, "password"); + mqtt_cfg.lwt_topic = CONFIG_MQTT_LWT_TOPIC; + mqtt_cfg.lwt_msg = "0"; + mqtt_cfg.lwt_qos = MQTT_QOS_0; + mqtt_cfg.lwt_retain = MQTT_RETAIN; + + nvs_close(nvs); + + client = esp_mqtt_client_init(&mqtt_cfg); + esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, client); + esp_mqtt_client_start(client); +} diff --git a/main/mqtt.h b/main/mqtt.h new file mode 100644 index 0000000..8dbd8bd --- /dev/null +++ b/main/mqtt.h @@ -0,0 +1,22 @@ +#ifndef __MQTT_H__ +#define __MQTT_H__ + +#define MQTT_TYPE_UNDEFINED 0 +#define MQTT_TYPE_STRING 1 +#define MQTT_TYPE_INT 2 +#define MQTT_TYPE_FLOAT 3 + +typedef struct { + int type; + union { + char* str; + float f; + int i; + } value; +} json_value; + + +void mqtt_init(); +void mqtt_publish_data(char* key, json_value value); + +#endif diff --git a/main/ota.c b/main/ota.c new file mode 100644 index 0000000..fdeb1ca --- /dev/null +++ b/main/ota.c @@ -0,0 +1,94 @@ +#include +#include "freertos/FreeRTOS.h" +#include "esp_log.h" +#include "esp_https_ota.h" +#include "esp_ota_ops.h" +#include "ota.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "common.h" + +static const char *OTA_LOGGER = "ota_update"; + +// Embedded via component.mk +extern const uint8_t cacert_pem_start[] asm("_binary_cacert_pem_start"); +extern const uint8_t cacert_pem_end[] asm("_binary_cacert_pem_end"); + +esp_err_t do_firmware_upgrade(char* firmware_url) { + esp_http_client_config_t config = { + .url = firmware_url, + .cert_pem = (char *)cacert_pem_start, + }; + + esp_https_ota_config_t ota_config = { + // Can be enabled in an upcoming version of the Espressif SDK + // to save some memory. + // Requires CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=4096 in sdkconfig. + // + //.partial_http_download = true, + //.max_http_request_size = 4096, + .http_config = &config, + }; + + esp_https_ota_handle_t https_ota_handle = NULL; + esp_err_t err = esp_https_ota_begin(&ota_config, &https_ota_handle); + if (https_ota_handle == NULL) { + return ESP_FAIL; + } + + while (1) { + err = esp_https_ota_perform(https_ota_handle); + if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) { + break; + } + } + + if (err != ESP_OK) { + esp_https_ota_abort(https_ota_handle); + return err; + } + + esp_err_t ota_finish_err = esp_https_ota_finish(https_ota_handle); + if (ota_finish_err != ESP_OK) { + return ota_finish_err; + } + + esp_restart(); // this function never returns + + return ESP_OK; +} + +void trigger_ota_update(char* version) { + const esp_app_desc_t* current = esp_ota_get_app_description(); + ESP_LOGD(OTA_LOGGER, "Currently running %s version %s", current->project_name, current->version); + + if (strcmp(version, current->version) == 0) { + // already to latest version + return; + } + + nvs_handle_t nvs; + esp_err_t err = nvs_open("ota", NVS_READONLY, &nvs); + if (err != ESP_OK) { + ESP_LOGE(OTA_LOGGER, "Error (%s) opening NVS handle!\n", esp_err_to_name(err)); + return; + } + char* update_url_pattern = get_nvs_string(nvs, "update_url"); + nvs_close(nvs); + if (update_url_pattern == NULL) { + return; + } + + const size_t buffer_size = 256; + char update_url[buffer_size]; + + // Format the update URL + if (!snprintf(update_url, buffer_size, update_url_pattern, version)) { + ESP_LOGD(OTA_LOGGER, "trigger_ota_update: snprintf failed!"); + free(update_url_pattern); + return; + } + + esp_err_t ret = do_firmware_upgrade(update_url); + ESP_LOGW(OTA_LOGGER, "do_firmware_upgrade failed with error %d", ret); +} diff --git a/main/ota.h b/main/ota.h new file mode 100644 index 0000000..4ec3a69 --- /dev/null +++ b/main/ota.h @@ -0,0 +1,6 @@ +#ifndef __OTA_H__ +#define __OTA_H__ + +void trigger_ota_update(char* version); + +#endif diff --git a/main/sntp.c b/main/sntp.c new file mode 100644 index 0000000..44cb4ed --- /dev/null +++ b/main/sntp.c @@ -0,0 +1,38 @@ +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_attr.h" +#include "esp_sntp.h" +#include "sntp.h" +#include "common.h" + +static const char *SNTP_LOGGER = "sntp"; + +void sntp_callback(struct timeval *tv) { + if (sntp_get_sync_mode() == SNTP_SYNC_MODE_IMMED) { + time_t now; + time(&now); + struct tm now_tm; + localtime_r(&now, &now_tm); + char strftime_buf[64]; + if (strftime(strftime_buf, sizeof(strftime_buf), "%c", &now_tm)) { + ESP_LOGI(SNTP_LOGGER, "Time synchronized: %s", strftime_buf); + } + sntp_set_sync_mode(SNTP_SYNC_MODE_SMOOTH); + xEventGroupSetBits(services_event_group, TIME_SYNC_BIT); + } +} + +void sntp_start() { + ESP_LOGI(SNTP_LOGGER, "Initializing SNTP..."); + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, "pool.ntp.org"); + sntp_set_time_sync_notification_cb(sntp_callback); + sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED); + sntp_init(); +} diff --git a/main/sntp.h b/main/sntp.h new file mode 100644 index 0000000..929f338 --- /dev/null +++ b/main/sntp.h @@ -0,0 +1,6 @@ +#ifndef __SNTP_H__ +#define __SNTP_H__ + +void sntp_start(); + +#endif diff --git a/main/solar.c b/main/solar.c new file mode 100644 index 0000000..b0046a3 --- /dev/null +++ b/main/solar.c @@ -0,0 +1,299 @@ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "solar.h" +#include "mqtt.h" +#include "common.h" + +// 1-Wire support +#include "owb.h" +#include "owb_rmt.h" +#include "ds18b20.h" + +// PWM support +#include "driver/gpio.h" +#include "driver/ledc.h" + +#define SC_MAX_TEMPERATURE_SENSORS_COUNT 8 +#define SC_EXPECTED_TEMPERATURE_SENSORS_COUNT 4 + +#define SC_PUMP_FULL_POWER 255 +#define SC_PUMP_HALF_POWER 150 +#define SC_PUMP_NO_POWER 0 + +#define SC_LEDC_CHANNEL_PANEL_PUMP LEDC_CHANNEL_0 +#define SC_LEDC_CHANNEL_FLOOR_HEATING_PUMP LEDC_CHANNEL_1 + +static const char *SOLAR_LOGGER = "solar"; +static DS18B20_Info temperature_sensors[SC_EXPECTED_TEMPERATURE_SENSORS_COUNT]; +static int panel; +static int store_high; +static int store_low; +static int floor_heating; +static OneWireBus * owb; +static owb_rmt_driver_info rmt_driver_info; +static int is_heating_enabled = 0; + +void solar_set_heating(int enabled) { + is_heating_enabled = enabled; + + if (IS_READY(MQTT_CONNECTED_BIT|TIME_SYNC_BIT)) { + mqtt_publish_data("floor_heating_enabled", (json_value){MQTT_TYPE_INT, {.i = is_heating_enabled}}); + } +} + +static int map_pwm(float delta_temp) { + float res = delta_temp * 10.2; + + if (res > 255) { + return 255; + } + + return res; +} + +static void solar_process(void *pvParameters) { + TickType_t last_wake_time = xTaskGetTickCount(); + int is_protecting_from_frost = 0; + int is_loading = 0; + int is_heating = 0; + int panel_pump_pwm = 0; + int floor_heating_pump_pwm = 0; + + int errors_count[SC_EXPECTED_TEMPERATURE_SENSORS_COUNT] = {0}; + for (;;) { + float readings[SC_EXPECTED_TEMPERATURE_SENSORS_COUNT]; + DS18B20_ERROR errors[SC_EXPECTED_TEMPERATURE_SENSORS_COUNT] = { 0 }; + + // Read temperatures more efficiently by starting conversions on all devices at the same time + ds18b20_convert_all(owb); + + // In this application all devices use the same resolution, + // so use the first device to determine the delay + ds18b20_wait_for_conversion(&temperature_sensors[0]); + + + // Read the results immediately after conversion otherwise it may fail + // (using printf before reading may take too long) + for (int i = 0; i < SC_EXPECTED_TEMPERATURE_SENSORS_COUNT; ++i) { + errors[i] = ds18b20_read_temp(&temperature_sensors[i], &readings[i]); + } + + // Check if there are errors during measure or data transmission + int has_reading_errors = 0; + for (int i = 0; i < SC_EXPECTED_TEMPERATURE_SENSORS_COUNT; ++i) { + if (errors[i] != DS18B20_OK) { + char rom_code_s[17]; + owb_string_from_rom_code(temperature_sensors[i].rom_code, rom_code_s, sizeof(rom_code_s)); + ESP_LOGW(SOLAR_LOGGER, "Error reading sensor %s.", rom_code_s); + ++errors_count[i]; + has_reading_errors = 1; + } + } + // And retry a measure if there were errors + if (has_reading_errors) { + ESP_LOGW(SOLAR_LOGGER, "There were errors while reading sensors. Retrying!"); + continue; + } + + float delta_panel = readings[panel] - readings[store_low]; + float delta_floor_heating = readings[store_high] - readings[floor_heating]; + + if (!is_protecting_from_frost && readings[panel] < CONFIG_SC_FROST_PROTECTION_TEMP_LOW) { + ESP_LOGI(SOLAR_LOGGER, "Temperature is running low in the panel (%.1f), engaging frost protection!", readings[panel]); + is_protecting_from_frost = 1; + panel_pump_pwm = SC_PUMP_HALF_POWER; + } else if (is_protecting_from_frost && readings[panel] > CONFIG_SC_FROST_PROTECTION_TEMP_HIGH) { + ESP_LOGI(SOLAR_LOGGER, "Disengaging frost protection..."); + is_protecting_from_frost = 0; + panel_pump_pwm = SC_PUMP_NO_POWER; + } else if (!is_loading && delta_panel > CONFIG_SC_PANEL_DELTA_TEMP_HIGH) { + ESP_LOGI(SOLAR_LOGGER, "Start loading heat from the panel to the store..."); + panel_pump_pwm = map_pwm(delta_panel); + is_loading = 1; + } else if (is_loading && delta_panel < CONFIG_SC_PANEL_DELTA_TEMP_LOW) { + ESP_LOGI(SOLAR_LOGGER, "Stop loading heat from the panel to the store..."); + panel_pump_pwm = SC_PUMP_NO_POWER; + is_loading = 0; + } else if (is_loading) { + // Keep adjusting the pump duty cycle according to the temperature difference + panel_pump_pwm = map_pwm(delta_panel); + } + + if (is_heating && !is_heating_enabled) { + ESP_LOGI(SOLAR_LOGGER, "Stop heating the floor (as requested)."); + floor_heating_pump_pwm = SC_PUMP_NO_POWER; + is_heating = 0; + } else if (is_heating_enabled && !is_heating && delta_floor_heating > CONFIG_SC_FLOOR_HEATING_DELTA_TEMP_HIGH) { + ESP_LOGI(SOLAR_LOGGER, "Start heating the floor from the store..."); + floor_heating_pump_pwm = SC_PUMP_HALF_POWER; + is_heating = 1; + } else if (is_heating && delta_floor_heating < CONFIG_SC_FLOOR_HEATING_DELTA_TEMP_LOW) { + ESP_LOGI(SOLAR_LOGGER, "Stop heating the floor from the store..."); + floor_heating_pump_pwm = SC_PUMP_NO_POWER; + is_heating = 0; + } + + ESP_LOGI(SOLAR_LOGGER, "p:%2.1f°C, s.l:%2.1f°C, s.h:%2.1f°C, f.h:%2.1f°C, p.p: %3.0f%%, fh.p: %3.0f%%", readings[panel], readings[store_low], readings[store_high], readings[floor_heating], panel_pump_pwm / 2.55, floor_heating_pump_pwm / 2.55); + + // Set new PWM duty cycle on each pump + ESP_ERROR_CHECK(ledc_set_duty(LEDC_HIGH_SPEED_MODE, SC_LEDC_CHANNEL_PANEL_PUMP, panel_pump_pwm)); + ESP_ERROR_CHECK(ledc_set_duty(LEDC_HIGH_SPEED_MODE, SC_LEDC_CHANNEL_FLOOR_HEATING_PUMP, floor_heating_pump_pwm)); + ESP_ERROR_CHECK(ledc_update_duty(LEDC_HIGH_SPEED_MODE, SC_LEDC_CHANNEL_PANEL_PUMP)); + ESP_ERROR_CHECK(ledc_update_duty(LEDC_HIGH_SPEED_MODE, SC_LEDC_CHANNEL_FLOOR_HEATING_PUMP)); + + // Only publish data when connected to the MQTT broker and time is synchronized with NTP + if (IS_READY(MQTT_CONNECTED_BIT|TIME_SYNC_BIT)) { + mqtt_publish_data("solar_panel_temperature", (json_value){MQTT_TYPE_FLOAT, {.f = readings[panel]}}); + mqtt_publish_data("floor_temperature", (json_value){MQTT_TYPE_FLOAT, {.f = readings[floor_heating]}}); + mqtt_publish_data("store_higher_temperature", (json_value){MQTT_TYPE_FLOAT, {.f = readings[store_high]}}); + mqtt_publish_data("store_lower_temperature", (json_value){MQTT_TYPE_FLOAT, {.f = readings[store_low]}}); + mqtt_publish_data("panel_pump_duty_cycle", (json_value){MQTT_TYPE_INT, {.i = panel_pump_pwm}}); + mqtt_publish_data("floor_heating_pump_duty_cycle", (json_value){MQTT_TYPE_INT, {.i = floor_heating_pump_pwm}}); + } + + vTaskDelayUntil(&last_wake_time, (1000 * CONFIG_SC_SAMPLE_PERIOD) / portTICK_PERIOD_MS); + } + vTaskDelete(NULL); +} + +static void solar_pwm_init() { + // Prepare and then apply the LEDC PWM timer configuration + ledc_timer_config_t ledc_timer_0 = { + .speed_mode = LEDC_HIGH_SPEED_MODE, + .timer_num = LEDC_TIMER_0, + .duty_resolution = LEDC_TIMER_8_BIT, + .freq_hz = CONFIG_SC_PUMP_PWM_FREQ, + .clk_cfg = LEDC_AUTO_CLK + }; + ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer_0)); + + // Prepare and then apply the LEDC PWM channel configuration + ledc_channel_config_t ledc_channel_0 = { + .speed_mode = LEDC_HIGH_SPEED_MODE, + .channel = LEDC_CHANNEL_0, + .timer_sel = LEDC_TIMER_0, + .intr_type = LEDC_INTR_DISABLE, + .gpio_num = CONFIG_SC_PANEL_PUMP_GPIO, + .duty = 0, // Set duty to 0% + .hpoint = 0 + }; + ledc_channel_config_t ledc_channel_1 = { + .speed_mode = LEDC_HIGH_SPEED_MODE, + .channel = LEDC_CHANNEL_1, + .timer_sel = LEDC_TIMER_0, + .intr_type = LEDC_INTR_DISABLE, + .gpio_num = CONFIG_SC_FLOOR_HEATING_PUMP_GPIO, + .duty = 0, // Set duty to 0% + .hpoint = 0 + }; + ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel_0)); + ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel_1)); +} + +static int sensor_index(char * address) { + for (int i = 0; i < SC_EXPECTED_TEMPERATURE_SENSORS_COUNT; i++) { + char rom_code_s[17]; + owb_string_from_rom_code(temperature_sensors[i].rom_code, rom_code_s, sizeof(rom_code_s)); + if (strcmp(address, rom_code_s) == 0) { + return i; + } + } + return -1; +} + +static void solar_ds18b20_init() { + // Stable readings require a brief period before communication + ESP_LOGI(SOLAR_LOGGER, "Waiting before 1-wire enumeration..."); + vTaskDelay(2000.0 / portTICK_PERIOD_MS); + + // Create a 1-Wire bus, using the RMT timeslot driver + owb = owb_rmt_initialize(&rmt_driver_info, CONFIG_SC_1WIRE_GPIO, RMT_CHANNEL_1, RMT_CHANNEL_0); + owb_use_crc(owb, true); // enable CRC check for ROM code + + // Find all connected temperature sensors + ESP_LOGI(SOLAR_LOGGER, "List of all DS18B20 devices on bus:"); + OneWireBus_ROMCode device_rom_codes[SC_MAX_TEMPERATURE_SENSORS_COUNT] = {0}; + int num_devices = 0; + OneWireBus_SearchState search_state = {0}; + bool found = false; + owb_search_first(owb, &search_state, &found); + + while (found) { + char rom_code_s[17]; + owb_string_from_rom_code(search_state.rom_code, rom_code_s, sizeof(rom_code_s)); + ESP_LOGI(SOLAR_LOGGER, "device %d: %s", num_devices, rom_code_s); + device_rom_codes[num_devices] = search_state.rom_code; + ++num_devices; + owb_search_next(owb, &search_state, &found); + } + ESP_LOGI(SOLAR_LOGGER, "Found %d device%s", num_devices, num_devices == 1 ? "" : "s"); + + if (num_devices != SC_EXPECTED_TEMPERATURE_SENSORS_COUNT) { + ESP_LOGE(SOLAR_LOGGER, "Cannot find exactly %d temperature sensors, rebooting!", SC_EXPECTED_TEMPERATURE_SENSORS_COUNT); + vTaskDelay(5000.0 / portTICK_PERIOD_MS); + esp_restart(); + } + + // Initializes all temperature sensors + for (int i = 0; i < num_devices; ++i) + { + ds18b20_init(&temperature_sensors[i], owb, device_rom_codes[i]); // associate with bus and device + ds18b20_use_crc(&temperature_sensors[i], true); // enable CRC check on all reads + ds18b20_set_resolution(&temperature_sensors[i], DS18B20_RESOLUTION_12_BIT); // use max. resolution + } + + // Extract sensor addresses from NVS + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open("solar", NVS_READONLY, &nvs)); + char * panel_sensor_addr = get_nvs_string(nvs, "panel_sensor"); + assert(panel_sensor_addr != NULL); + char * store_higher_sensor_addr = get_nvs_string(nvs, "store_h_sensor"); + assert(store_higher_sensor_addr != NULL); + char * store_lower_sensor_addr = get_nvs_string(nvs, "store_l_sensor"); + assert(store_lower_sensor_addr != NULL); + char * floor_heating_sensor_addr = get_nvs_string(nvs, "fl_ht_sensor"); + assert(floor_heating_sensor_addr != NULL); + + if (panel_sensor_addr == NULL || store_higher_sensor_addr == NULL || store_lower_sensor_addr == NULL || floor_heating_sensor_addr == NULL) { + ESP_LOGE(SOLAR_LOGGER, "Cannot find one of the temperature sensor addresses, check your configuration!"); + vTaskDelay(5000.0 / portTICK_PERIOD_MS); + esp_restart(); + } + + // Match each sensor with its function + panel = sensor_index(panel_sensor_addr); + assert(panel != -1); + store_high = sensor_index(store_higher_sensor_addr); + assert(store_high != -1); + store_low = sensor_index(store_lower_sensor_addr); + assert(store_low != -1); + floor_heating = sensor_index(floor_heating_sensor_addr); + assert(floor_heating != -1); + + if (panel == -1 || store_high == -1 || store_low == -1 || floor_heating == -1) { + ESP_LOGE(SOLAR_LOGGER, "Cannot find one of the temperature sensors, check your configuration!"); + vTaskDelay(5000.0 / portTICK_PERIOD_MS); + esp_restart(); + } +} + +void solar_init() { + solar_pwm_init(); + solar_ds18b20_init(); + + // Create a task to handle solar monitoring + BaseType_t xReturned; + xReturned = xTaskCreate(solar_process, + "solar_process", + CONFIG_SC_STACK_SIZE, /* Stack size in words, not bytes. */ + NULL, /* Parameter passed into the task. */ + tskIDLE_PRIORITY + 12, + NULL); + if (xReturned != pdPASS) { + ESP_LOGE(SOLAR_LOGGER, "xTaskCreate('solar_process'): %d", xReturned); + abort(); + } +} diff --git a/main/solar.h b/main/solar.h new file mode 100644 index 0000000..46715eb --- /dev/null +++ b/main/solar.h @@ -0,0 +1,7 @@ +#ifndef __SOLAR_H__ +#define __SOLAR_H__ + +void solar_init(); +void solar_set_heating(int enabled); + +#endif diff --git a/main/wifi.c b/main/wifi.c new file mode 100644 index 0000000..7f29c45 --- /dev/null +++ b/main/wifi.c @@ -0,0 +1,61 @@ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_log.h" +#include "nvs_flash.h" + +#include "lwip/err.h" +#include "lwip/sys.h" + +#include "wifi.h" +#include "common.h" + +static const char *WIFI_LOGGER = "wifi"; + +static void wifi_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) { + ESP_LOGI(WIFI_LOGGER,"Connection to the AP failed!"); + xEventGroupClearBits(services_event_group, WIFI_CONNECTED_BIT); + + ESP_LOGI(WIFI_LOGGER, "Retrying to connect to the AP..."); + esp_wifi_connect(); + } 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(WIFI_LOGGER, "got ip:" IPSTR, IP2STR(&event->ip_info.ip)); + xEventGroupSetBits(services_event_group, WIFI_CONNECTED_BIT); + } +} + +void wifi_init_sta(void) { + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_ERROR_CHECK(esp_event_loop_create_default()); + 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, + &wifi_event_handler, + NULL, + &instance_any_id)); + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, + IP_EVENT_STA_GOT_IP, + &wifi_event_handler, + NULL, + &instance_got_ip)); + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); + + ESP_LOGI(WIFI_LOGGER, "wifi_init_sta finished."); +} diff --git a/main/wifi.h b/main/wifi.h new file mode 100644 index 0000000..73d6f67 --- /dev/null +++ b/main/wifi.h @@ -0,0 +1,6 @@ +#ifndef __WIFI_H__ +#define __WIFI_H__ + +void wifi_init_sta(void); + +#endif diff --git a/provision/.gitignore b/provision/.gitignore new file mode 100644 index 0000000..a120452 --- /dev/null +++ b/provision/.gitignore @@ -0,0 +1,5 @@ +# contains sensitive data +sdkconfig +sdkconfig.dev +sdkconfig.prod +sdkconfig.defaults diff --git a/provision/CMakeLists.txt b/provision/CMakeLists.txt new file mode 100644 index 0000000..8c76a81 --- /dev/null +++ b/provision/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(provisioner) diff --git a/provision/Makefile b/provision/Makefile new file mode 100644 index 0000000..4ccc640 --- /dev/null +++ b/provision/Makefile @@ -0,0 +1,8 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := provisioner + +include $(IDF_PATH)/make/project.mk diff --git a/provision/README.md b/provision/README.md new file mode 100644 index 0000000..ad2c06e --- /dev/null +++ b/provision/README.md @@ -0,0 +1,2 @@ +# Settings provisioner + diff --git a/provision/main/CMakeLists.txt b/provision/main/CMakeLists.txt new file mode 100644 index 0000000..0abd594 --- /dev/null +++ b/provision/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "provision.c" + INCLUDE_DIRS ".") diff --git a/provision/main/Kconfig.projbuild b/provision/main/Kconfig.projbuild new file mode 100644 index 0000000..82f1588 --- /dev/null +++ b/provision/main/Kconfig.projbuild @@ -0,0 +1,68 @@ +menu "Provisioning data" + + config ESP_WIFI_SSID + string "WiFi SSID" + default "" + help + SSID (network name) for the example to connect to. + + config ESP_WIFI_PASSWORD + string "WiFi Password" + default "" + help + WiFi password (WPA or WPA2) for the example to use. + + config ESP_MAXIMUM_RETRY + int "Maximum retry" + default 5 + help + Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent. + + config MQTT_URI + string "MQTT Server URI" + default "mqtts://server:port" + help + MQTT server location. + + config MQTT_USERNAME + string "MQTT username" + default "" + help + MQTT username. + + config MQTT_PASSWORD + string "MQTT password" + default "" + help + MQTT password. + + config FIRMWARE_URL_PATTERN + string "Firmware URL pattern" + default "" + help + Where to download a specific version of the firmware. Complete URL. Must include "%s". + + config PANEL_SENSOR_ADDR + string "1-wire address of the panel sensor" + default "" + help + 1-wire address of the panel sensor. + + config STORE_LOWER_SENSOR_ADDR + string "1-wire address of the store lower sensor" + default "" + help + 1-wire address of the store lower sensor. + + config STORE_HIGHER_SENSOR_ADDR + string "1-wire address of the store higher sensor" + default "" + help + 1-wire address of the store higher sensor. + + config FLOOR_HEATING_SENSOR_ADDR + string "1-wire address of the floor heating sensor" + default "" + help + 1-wire address of the floor heating sensor. +endmenu diff --git a/provision/main/component.mk b/provision/main/component.mk new file mode 100644 index 0000000..0adf456 --- /dev/null +++ b/provision/main/component.mk @@ -0,0 +1,8 @@ +# +# Main component makefile. +# +# This Makefile can be left empty. By default, it will take the sources in the +# src/ directory, compile them and link them into lib(subdirectory_name).a +# in the build directory. This behaviour is entirely configurable, +# please read the ESP-IDF documents if you need to do this. +# diff --git a/provision/main/provision.c b/provision/main/provision.c new file mode 100644 index 0000000..d9740ea --- /dev/null +++ b/provision/main/provision.c @@ -0,0 +1,169 @@ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_log.h" +#include "nvs_flash.h" + +#include "lwip/err.h" +#include "lwip/sys.h" + +#include "nvs_flash.h" +#include "nvs.h" + +/* FreeRTOS event group to signal when we are connected*/ +static EventGroupHandle_t s_wifi_event_group; + +/* The event group allows multiple bits for each event, but we only care about two events: + * - we are connected to the AP with an IP + * - we failed to connect after the maximum amount of retries */ +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static const char *TAG = "wifi station"; + +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 < CONFIG_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 nvs_dumpall() { + printf("NVS Dump: \n"); + nvs_iterator_t it = nvs_entry_find("nvs", NULL, NVS_TYPE_ANY); + while (it != NULL) { + char * value = NULL; + char namespace[17]; + memset(namespace, 0, 17); + nvs_entry_info_t info; + nvs_entry_info(it, &info); + memcpy(namespace, info.namespace_name, 16); + printf("%s/%s: type 0x%02x, value = %s\n", namespace, info.key, info.type, value ? value : ""); + it = nvs_entry_next(it); + }; +} + +void wifi_init_sta(void) +{ + s_wifi_event_group = xEventGroupCreate(); + + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_ERROR_CHECK(esp_event_loop_create_default()); + 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 = CONFIG_ESP_WIFI_SSID, + .password = CONFIG_ESP_WIFI_PASSWORD, + .threshold.authmode = WIFI_AUTH_WPA2_PSK, + + .pmf_cfg = { + .capable = true, + .required = false + }, + }, + }; + 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", + CONFIG_ESP_WIFI_SSID, CONFIG_ESP_WIFI_PASSWORD); + } else if (bits & WIFI_FAIL_BIT) { + ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", + CONFIG_ESP_WIFI_SSID, CONFIG_ESP_WIFI_PASSWORD); + } else { + ESP_LOGE(TAG, "UNEXPECTED EVENT"); + } + + nvs_handle_t nvs; + ESP_ERROR_CHECK(nvs_open("mqtt", NVS_READWRITE, &nvs)); + nvs_set_str(nvs, "url", CONFIG_MQTT_URI); + nvs_set_str(nvs, "username", CONFIG_MQTT_USERNAME); + nvs_set_str(nvs, "password", CONFIG_MQTT_PASSWORD); + nvs_close(nvs); + + ESP_ERROR_CHECK(nvs_open("ota", NVS_READWRITE, &nvs)); + nvs_set_str(nvs, "update_url", CONFIG_FIRMWARE_URL_PATTERN); + nvs_close(nvs); + + ESP_ERROR_CHECK(nvs_open("solar", NVS_READWRITE, &nvs)); + nvs_set_str(nvs, "panel_sensor", CONFIG_PANEL_SENSOR_ADDR); + nvs_set_str(nvs, "store_h_sensor", CONFIG_STORE_HIGHER_SENSOR_ADDR); + nvs_set_str(nvs, "store_l_sensor", CONFIG_STORE_LOWER_SENSOR_ADDR); + nvs_set_str(nvs, "fl_ht_sensor", CONFIG_FLOOR_HEATING_SENSOR_ADDR); + nvs_close(nvs); + + nvs_dumpall(); + + /* The event will not be processed after unregister */ + ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip)); + ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id)); + vEventGroupDelete(s_wifi_event_group); +} + +void app_main(void) +{ + //Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ESP_LOGI(TAG, "ESP_WIFI_MODE_STA"); + wifi_init_sta(); +} diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 120000 index 0000000..e268593 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1 @@ +sdkconfig.dev \ No newline at end of file diff --git a/sdkconfig.dev b/sdkconfig.dev new file mode 100644 index 0000000..2aaff44 --- /dev/null +++ b/sdkconfig.dev @@ -0,0 +1,19 @@ +CONFIG_MQTT_LWT_TOPIC="test/solar-controller/connected" +CONFIG_MQTT_SOLAR_VALUE_TOPIC="test/solar-controller/status/solar/%s" +CONFIG_MQTT_COMMAND_TOPIC="test/solar-controller/command" +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_TWO_OTA=y +CONFIG_LOG_DEFAULT_LEVEL=4 +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y +CONFIG_OTA_ALLOW_HTTP=y +CONFIG_SC_1WIRE_GPIO=4 +CONFIG_SC_SAMPLE_PERIOD=1 +CONFIG_SC_PANEL_PUMP_GPIO=19 +CONFIG_SC_FLOOR_HEATING_PUMP_GPIO=21 +CONFIG_SC_FROST_PROTECTION_TEMP_LOW=16 +CONFIG_SC_FROST_PROTECTION_TEMP_HIGH=18 +CONFIG_SC_PANEL_DELTA_TEMP_LOW=2 +CONFIG_SC_PANEL_DELTA_TEMP_HIGH=3 +CONFIG_SC_FLOOR_HEATING_DELTA_TEMP_LOW=2 +CONFIG_SC_FLOOR_HEATING_DELTA_TEMP_HIGH=3 diff --git a/sdkconfig.prod b/sdkconfig.prod new file mode 100644 index 0000000..a1e8156 --- /dev/null +++ b/sdkconfig.prod @@ -0,0 +1,9 @@ +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_TWO_OTA=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y +# CONFIG_MQTT_TRANSPORT_WEBSOCKET is not set +# CONFIG_LWIP_IPV6 is not set +# CONFIG_ETH_USE_ESP32_EMAC is not set +# CONFIG_ETH_USE_SPI_ETHERNET is not set