From 618a6974bf03ce57cedbeaeab04e7286bec4faf4f025ab545417f2ca95d4e00a Mon Sep 17 00:00:00 2001 From: localhorst Date: Fri, 9 Jan 2026 23:26:46 +0100 Subject: [PATCH 1/6] enable ap lock --- main/Kconfig.projbuild | 14 ++++++++++++++ main/wifi.c | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index c855f47..5b17d67 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -18,5 +18,19 @@ menu "Smart Oil Heating Control System" config SNTP_SERVER_IP_ADDR string "SNTP IPv4 server address" default "192.168.0.1" + config ENV_WIFI_BSSID_LOCK + bool "Lock to specific Access Point (BSSID)" + default n + help + When enabled, the device will only connect to the access point + with the specified MAC address (BSSID). Useful when multiple APs + share the same SSID. + config ENV_WIFI_BSSID + string "Access Point MAC Address (BSSID)" + default "00:00:00:00:00:00" + depends on ENV_WIFI_BSSID_LOCK + help + MAC address of the access point to connect to. + Format: XX:XX:XX:XX:XX:XX (uppercase or lowercase) endmenu diff --git a/main/wifi.c b/main/wifi.c index d1fd0c1..d97e472 100644 --- a/main/wifi.c +++ b/main/wifi.c @@ -19,6 +19,7 @@ static const char *TAG = "smart-oil-heater-control-system-wifi"; static EventGroupHandle_t s_wifi_event_group; static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); +static bool parse_bssid(const char *bssid_str, uint8_t *bssid); void initWifi(void) { @@ -56,6 +57,21 @@ void initWifi(void) .threshold.authmode = WIFI_AUTH_WPA2_PSK, }, }; + +#if CONFIG_ENV_WIFI_BSSID_LOCK + /* Lock to specific AP by BSSID */ + if (parse_bssid(CONFIG_ENV_WIFI_BSSID, wifi_config.sta.bssid)) + { + wifi_config.sta.bssid_set = true; + ESP_LOGI(TAG, "BSSID lock enabled: %s", CONFIG_ENV_WIFI_BSSID); + } + else + { + ESP_LOGE(TAG, "Invalid BSSID format: %s", CONFIG_ENV_WIFI_BSSID); + wifi_config.sta.bssid_set = false; + } +#endif + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); @@ -105,4 +121,28 @@ static void event_handler(void *arg, esp_event_base_t event_base, ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip)); xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } +} + +/** + * @brief Parse BSSID string to byte array + * + * @param bssid_str BSSID string in format "XX:XX:XX:XX:XX:XX" + * @param bssid Output byte array (6 bytes) + * @return true on success, false on parse error + */ +static bool parse_bssid(const char *bssid_str, uint8_t *bssid) +{ + unsigned int tmp[6]; + int parsed = sscanf(bssid_str, "%x:%x:%x:%x:%x:%x", + &tmp[0], &tmp[1], &tmp[2], + &tmp[3], &tmp[4], &tmp[5]); + if (parsed != 6) + { + return false; + } + for (int i = 0; i < 6; i++) + { + bssid[i] = (uint8_t)tmp[i]; + } + return true; } \ No newline at end of file From b77cba87ede27177feed62c69d92f9aa3222663f392ccf56d68cde8e86326f20 Mon Sep 17 00:00:00 2001 From: localhorst Date: Fri, 9 Jan 2026 23:44:53 +0100 Subject: [PATCH 2/6] fix wifi event handler --- main/wifi.c | 88 ++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/main/wifi.c b/main/wifi.c index d97e472..6a4b806 100644 --- a/main/wifi.c +++ b/main/wifi.c @@ -13,13 +13,17 @@ #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 +#define MAX_RETRY_COUNT 10 +#define RETRY_DELAY_MS 1000 static const char *TAG = "smart-oil-heater-control-system-wifi"; static EventGroupHandle_t s_wifi_event_group; +static int s_retry_num = 0; +static bool s_initial_connect = true; + static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); -static bool parse_bssid(const char *bssid_str, uint8_t *bssid); void initWifi(void) { @@ -57,21 +61,6 @@ void initWifi(void) .threshold.authmode = WIFI_AUTH_WPA2_PSK, }, }; - -#if CONFIG_ENV_WIFI_BSSID_LOCK - /* Lock to specific AP by BSSID */ - if (parse_bssid(CONFIG_ENV_WIFI_BSSID, wifi_config.sta.bssid)) - { - wifi_config.sta.bssid_set = true; - ESP_LOGI(TAG, "BSSID lock enabled: %s", CONFIG_ENV_WIFI_BSSID); - } - else - { - ESP_LOGE(TAG, "Invalid BSSID format: %s", CONFIG_ENV_WIFI_BSSID); - wifi_config.sta.bssid_set = false; - } -#endif - ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); @@ -100,7 +89,9 @@ void initWifi(void) { ESP_LOGE(TAG, "Unexpected event"); } - vEventGroupDelete(s_wifi_event_group); + + // Mark initial connection phase complete - do NOT delete the event group + s_initial_connect = false; } static void event_handler(void *arg, esp_event_base_t event_base, @@ -112,37 +103,46 @@ static void event_handler(void *arg, esp_event_base_t event_base, } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { - esp_wifi_connect(); - ESP_LOGI(TAG, "Retry to connect to the AP"); + wifi_event_sta_disconnected_t *event = (wifi_event_sta_disconnected_t *)event_data; + ESP_LOGW(TAG, "Disconnected from AP (reason: %d)", event->reason); + + if (s_initial_connect) + { + // During initial connection phase, use retry limit + if (s_retry_num < MAX_RETRY_COUNT) + { + vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS)); + esp_wifi_connect(); + s_retry_num++; + ESP_LOGI(TAG, "Retry to connect to the AP (%d/%d)", s_retry_num, MAX_RETRY_COUNT); + } + else + { + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + ESP_LOGE(TAG, "Failed to connect after %d attempts", MAX_RETRY_COUNT); + } + } + else + { + // After initial connection, always try to reconnect with delay + vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS)); + esp_wifi_connect(); + ESP_LOGI(TAG, "Attempting to reconnect to the AP..."); + } } 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)); - xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + s_retry_num = 0; + + if (s_initial_connect) + { + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } + else + { + ESP_LOGI(TAG, "Successfully reconnected to AP"); + } } } - -/** - * @brief Parse BSSID string to byte array - * - * @param bssid_str BSSID string in format "XX:XX:XX:XX:XX:XX" - * @param bssid Output byte array (6 bytes) - * @return true on success, false on parse error - */ -static bool parse_bssid(const char *bssid_str, uint8_t *bssid) -{ - unsigned int tmp[6]; - int parsed = sscanf(bssid_str, "%x:%x:%x:%x:%x:%x", - &tmp[0], &tmp[1], &tmp[2], - &tmp[3], &tmp[4], &tmp[5]); - if (parsed != 6) - { - return false; - } - for (int i = 0; i < 6; i++) - { - bssid[i] = (uint8_t)tmp[i]; - } - return true; -} \ No newline at end of file From c935eacbdd9bd5d2220e46453794e81715ff41f682e93f17f7e50c53ce9660ca Mon Sep 17 00:00:00 2001 From: localhorst Date: Sun, 25 Jan 2026 14:28:03 +0100 Subject: [PATCH 3/6] update to new IDF component lib (#31) https://components.espressif.com/components/esp-idf-lib/ds18x20/versions/1.2.8/readme Reviewed-on: https://git.mosad.xyz/localhorst/smart-oil-heating-control-system/pulls/31 Co-authored-by: localhorst Co-committed-by: localhorst --- .gitignore | 2 ++ CMakeLists.txt | 3 --- main/idf_component.yml | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 main/idf_component.yml diff --git a/.gitignore b/.gitignore index e5348d3..4d25375 100644 --- a/.gitignore +++ b/.gitignore @@ -291,3 +291,5 @@ dkms.conf *.out *.app +managed_components/ +dependencies.lock diff --git a/CMakeLists.txt b/CMakeLists.txt index 561307f..c67b51e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,3 @@ - -set(EXTRA_COMPONENT_DIRS $ENV{ESP_IDF_LIB_PATH}/components) - # 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.16) diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..5fa17bf --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + idf: + version: '>=4.1.0' + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true + esp-idf-lib/ds18x20: '*' From 5a223ddbe5389ac42d0be1104ed785f541447e2cde389dad85ec469bf44a934c Mon Sep 17 00:00:00 2001 From: localhorst Date: Sun, 25 Jan 2026 14:46:47 +0100 Subject: [PATCH 4/6] Lock to specific Access Point (#26) Reviewed-on: https://git.mosad.xyz/localhorst/smart-oil-heating-control-system/pulls/26 Co-authored-by: localhorst Co-committed-by: localhorst --- main/Kconfig.projbuild | 14 ++++++++++++++ main/wifi.c | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index c855f47..5b17d67 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -18,5 +18,19 @@ menu "Smart Oil Heating Control System" config SNTP_SERVER_IP_ADDR string "SNTP IPv4 server address" default "192.168.0.1" + config ENV_WIFI_BSSID_LOCK + bool "Lock to specific Access Point (BSSID)" + default n + help + When enabled, the device will only connect to the access point + with the specified MAC address (BSSID). Useful when multiple APs + share the same SSID. + config ENV_WIFI_BSSID + string "Access Point MAC Address (BSSID)" + default "00:00:00:00:00:00" + depends on ENV_WIFI_BSSID_LOCK + help + MAC address of the access point to connect to. + Format: XX:XX:XX:XX:XX:XX (uppercase or lowercase) endmenu diff --git a/main/wifi.c b/main/wifi.c index 6a4b806..05bc4c8 100644 --- a/main/wifi.c +++ b/main/wifi.c @@ -24,6 +24,7 @@ static bool s_initial_connect = true; static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); +static bool parse_bssid(const char *bssid_str, uint8_t *bssid); void initWifi(void) { @@ -61,6 +62,21 @@ void initWifi(void) .threshold.authmode = WIFI_AUTH_WPA2_PSK, }, }; + +#if CONFIG_ENV_WIFI_BSSID_LOCK + /* Lock to specific AP by BSSID */ + if (parse_bssid(CONFIG_ENV_WIFI_BSSID, wifi_config.sta.bssid)) + { + wifi_config.sta.bssid_set = true; + ESP_LOGI(TAG, "BSSID lock enabled: %s", CONFIG_ENV_WIFI_BSSID); + } + else + { + ESP_LOGE(TAG, "Invalid BSSID format: %s", CONFIG_ENV_WIFI_BSSID); + wifi_config.sta.bssid_set = false; + } +#endif + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); @@ -146,3 +162,27 @@ static void event_handler(void *arg, esp_event_base_t event_base, } } } + +/** + * @brief Parse BSSID string to byte array + * + * @param bssid_str BSSID string in format "XX:XX:XX:XX:XX:XX" + * @param bssid Output byte array (6 bytes) + * @return true on success, false on parse error + */ +static bool parse_bssid(const char *bssid_str, uint8_t *bssid) +{ + unsigned int tmp[6]; + int parsed = sscanf(bssid_str, "%x:%x:%x:%x:%x:%x", + &tmp[0], &tmp[1], &tmp[2], + &tmp[3], &tmp[4], &tmp[5]); + if (parsed != 6) + { + return false; + } + for (int i = 0; i < 6; i++) + { + bssid[i] = (uint8_t)tmp[i]; + } + return true; +} From 260b26023c3ae2b73117f17753c89a9f406aebacd788529cdbadbd06d3940283 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 14 Feb 2026 16:21:32 +0100 Subject: [PATCH 5/6] bugfix/static-code-analysis (#28) The claude.ai LLM performed a static code analysis. Reviewed-on: https://git.mosad.xyz/localhorst/smart-oil-heating-control-system/pulls/28 Co-authored-by: localhorst Co-committed-by: localhorst --- main/control.c | 208 +++++++++++++++++++++++++++++++------------------ main/inputs.c | 33 ++++++-- main/metrics.c | 8 +- main/outputs.c | 38 ++++++++- main/safety.c | 9 ++- main/sntp.c | 2 +- main/wifi.c | 10 ++- 7 files changed, 212 insertions(+), 96 deletions(-) diff --git a/main/control.c b/main/control.c index 1098bc7..32bc58a 100644 --- a/main/control.c +++ b/main/control.c @@ -25,9 +25,9 @@ (60U * 4U) // Burner fault detection after 4 minutes static const char *TAG = "smart-oil-heater-control-system-control"; -static eControlState sControlState = CONTROL_STARTING; +static eControlState gControlState = CONTROL_STARTING; // Control table for daily schedules -static const sControlDay aControlTable[] = { +static const sControlDay gControlTable[] = { {MONDAY, 2U, {{{4, 45}, @@ -85,15 +85,25 @@ static const sControlDay aControlTable[] = { RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMBER_TEMPERATURE_TARGET}}}, }; -static sControlTemperatureEntry currentControlEntry = - aControlTable[0].aTemperatureEntries[0]; +static sControlTemperatureEntry gCurrentControlEntry = + gControlTable[0].aTemperatureEntries[0]; +static SemaphoreHandle_t xMutexAccessControl = NULL; // Function prototypes void taskControl(void *pvParameters); void findControlCurrentTemperatureEntry(void); +void setControlState(eControlState state); void initControl(void) { + + xMutexAccessControl = xSemaphoreCreateRecursiveMutex(); + if (xMutexAccessControl == NULL) + { + ESP_LOGE(TAG, "Unable to create mutex"); + } + xSemaphoreGiveRecursive(xMutexAccessControl); + BaseType_t taskCreated = xTaskCreate(taskControl, // Function to implement the task "taskControl", // Task name @@ -117,7 +127,7 @@ void taskControl(void *pvParameters) { bool bHeatingInAction = false; bool bSummerMode = false; - eBurnerState eBurnerState = BURNER_UNKNOWN; + eBurnerState burnerState = BURNER_UNKNOWN; int64_t i64BurnerEnableTimestamp = esp_timer_get_time(); while (1) @@ -128,7 +138,7 @@ void taskControl(void *pvParameters) if (getSafetyState() != SAFETY_NO_ERROR) { ESP_LOGW(TAG, "Control not possible due to safety fault!"); - sControlState = CONTROL_FAULT_SAFETY; + setControlState(CONTROL_FAULT_SAFETY); if (bHeatingInAction) { ESP_LOGW(TAG, "Disabling burner due to safety fault"); @@ -143,7 +153,7 @@ void taskControl(void *pvParameters) if (getSntpState() != SYNC_SUCCESSFUL) { ESP_LOGW(TAG, "Control not possible due to SNTP fault!"); - sControlState = CONTROL_FAULT_SNTP; + setControlState(CONTROL_FAULT_SNTP); if (bHeatingInAction) { ESP_LOGW(TAG, "Disabling burner due to SNTP fault"); @@ -155,8 +165,6 @@ void taskControl(void *pvParameters) } findControlCurrentTemperatureEntry(); - sControlTemperatureEntry currentControlEntry = - getControlCurrentTemperatureEntry(); if (getOutdoorTemperature().fDampedValue >= SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH) @@ -171,33 +179,33 @@ void taskControl(void *pvParameters) // Enable burner if outdoor temperature is low and return flow temperature // is cooled down - if (!bHeatingInAction && (eBurnerState != BURNER_FAULT)) + if (!bHeatingInAction && (burnerState != BURNER_FAULT)) { if (bSummerMode) { // ESP_LOGI(TAG, "Outdoor temperature too warm: Disabling heating"); setBurnerState(DISABLED); setSafetyControlState(DISABLED); - sControlState = CONTROL_OUTDOOR_TOO_WARM; + setControlState(CONTROL_OUTDOOR_TOO_WARM); } else if ((getReturnFlowTemperature().average60s.fValue <= - currentControlEntry.fReturnFlowTemperature) && + getControlCurrentTemperatureEntry().fReturnFlowTemperature) && (getChamberTemperature().fCurrentValue <= CHAMBER_TEMPERATURE_THRESHOLD)) { ESP_LOGI(TAG, "Enabling burner: Return flow temperature target reached"); - eBurnerState = BURNER_UNKNOWN; + burnerState = BURNER_UNKNOWN; bHeatingInAction = true; setBurnerState(ENABLED); setSafetyControlState(ENABLED); i64BurnerEnableTimestamp = esp_timer_get_time(); - sControlState = CONTROL_HEATING; + setControlState(CONTROL_HEATING); } else { // ESP_LOGI(TAG, "Return flow temperature too warm: Disabling heating"); - sControlState = CONTROL_RETURN_FLOW_TOO_WARM; + setControlState(CONTROL_RETURN_FLOW_TOO_WARM); } } @@ -205,9 +213,9 @@ void taskControl(void *pvParameters) if (bHeatingInAction) { if ((getChamberTemperature().fCurrentValue >= - currentControlEntry.fChamberTemperature) || + getControlCurrentTemperatureEntry().fChamberTemperature) || (getChamberTemperature().predict60s.fValue >= - currentControlEntry.fChamberTemperature)) + getControlCurrentTemperatureEntry().fChamberTemperature)) { ESP_LOGI(TAG, "Chamber target temperature reached: Disabling burner"); bHeatingInAction = false; @@ -217,14 +225,14 @@ void taskControl(void *pvParameters) else if (esp_timer_get_time() - i64BurnerEnableTimestamp >= BURNER_FAULT_DETECTION_THRESHOLD * 1000000U) { - if (eBurnerState == BURNER_UNKNOWN) + if (burnerState == BURNER_UNKNOWN) { if (getBurnerError() == FAULT) { // ESP_LOGW(TAG, "Burner fault detected: Disabling burner"); bHeatingInAction = false; - eBurnerState = BURNER_FAULT; - sControlState = CONTROL_FAULT_BURNER; + burnerState = BURNER_FAULT; + setControlState(CONTROL_FAULT_BURNER); setBurnerState(DISABLED); setSafetyControlState(ENABLED); } @@ -232,7 +240,7 @@ void taskControl(void *pvParameters) { // ESP_LOGI(TAG, "No burner fault detected: Marking burner as // fired"); - eBurnerState = BURNER_FIRED; + burnerState = BURNER_FIRED; } } } @@ -253,17 +261,47 @@ void taskControl(void *pvParameters) } // End of while(1) } -eControlState getControlState(void) { return sControlState; } +void setControlState(eControlState state) +{ + + if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE) + { + gControlState = state; + xSemaphoreGiveRecursive(xMutexAccessControl); + } + else + { + ESP_LOGE(TAG, "Unable to take mutex: setControlState()"); + } +} + +eControlState getControlState(void) +{ + + eControlState ret = CONTROL_FAULT_SAFETY; + + if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE) + { + ret = gControlState; + xSemaphoreGiveRecursive(xMutexAccessControl); + } + else + { + ESP_LOGE(TAG, "Unable to take mutex: getControlState()"); + } + + return ret; +} eControlWeekday getControlCurrentWeekday(void) { + // Get current time time_t now; - struct tm *timeinfo; - + struct tm timeinfo; time(&now); - timeinfo = localtime(&now); + localtime_r(&now, &timeinfo); - int day = timeinfo->tm_wday; + int day = timeinfo.tm_wday; return (eControlWeekday)((day == 0) ? 6 : day - 1); } @@ -294,66 +332,88 @@ void findControlCurrentTemperatureEntry(void) int currentHour = timeinfo.tm_hour; int currentMinute = timeinfo.tm_min; - // ESP_LOGI(TAG, "Searching for control entry - Day: %d, Time: %02d:%02d", currentDay, currentHour, currentMinute); - - // Search through all days and entries - for (int dayIndex = 0; dayIndex < 7; dayIndex++) + if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE) { - const sControlDay *day = &aControlTable[dayIndex]; - for (int entryIndex = 0; entryIndex < day->entryCount; entryIndex++) + // ESP_LOGI(TAG, "Searching for control entry - Day: %d, Time: %02d:%02d", currentDay, currentHour, currentMinute); + + // Search through all days and entries + for (int dayIndex = 0; dayIndex < 7; dayIndex++) { - const sControlTemperatureEntry *entry = &day->aTemperatureEntries[entryIndex]; + const sControlDay *day = &gControlTable[dayIndex]; - // Check if this entry is in the future (next active entry) - bool isFutureDay = (day->day > currentDay); - bool isTodayFutureTime = (day->day == currentDay) && - ((entry->timestamp.hour > currentHour) || - (entry->timestamp.hour == currentHour && - entry->timestamp.minute > currentMinute)); - - if (isFutureDay || isTodayFutureTime) + for (int entryIndex = 0; entryIndex < day->entryCount; entryIndex++) { - // Found next scheduled entry, so determine the previous (active) one - if (entryIndex > 0) + const sControlTemperatureEntry *entry = &day->aTemperatureEntries[entryIndex]; + + // Check if this entry is in the future (next active entry) + bool isFutureDay = (day->day > currentDay); + bool isTodayFutureTime = (day->day == currentDay) && + ((entry->timestamp.hour > currentHour) || + (entry->timestamp.hour == currentHour && + entry->timestamp.minute > currentMinute)); + + if (isFutureDay || isTodayFutureTime) { - // Use previous entry from same day - currentControlEntry = day->aTemperatureEntries[entryIndex - 1]; + + // Found next scheduled entry, so determine the previous (active) one + if (entryIndex > 0) + { + // Use previous entry from same day + gCurrentControlEntry = day->aTemperatureEntries[entryIndex - 1]; + } + else if (dayIndex > 0) + { + // Use last entry from previous day + const sControlDay *previousDay = &gControlTable[dayIndex - 1]; + gCurrentControlEntry = previousDay->aTemperatureEntries[previousDay->entryCount - 1]; + } + else + { + // First entry of the week - wrap to last entry of Sunday + const sControlDay *sunday = &gControlTable[6]; + gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1]; + } + /* + ESP_LOGI(TAG, "Active entry found - Time: %02d:%02d, " + "Return Temp: %lf, Chamber Temp: %lf", + gCurrentControlEntry.timestamp.hour, + gCurrentControlEntry.timestamp.minute, + gCurrentControlEntry.fReturnFlowTemperature, + gCurrentControlEntry.fChamberTemperature); + */ + xSemaphoreGiveRecursive(xMutexAccessControl); + return; } - else if (dayIndex > 0) - { - // Use last entry from previous day - const sControlDay *previousDay = &aControlTable[dayIndex - 1]; - currentControlEntry = previousDay->aTemperatureEntries[previousDay->entryCount - 1]; - } - else - { - // First entry of the week - wrap to last entry of Sunday - const sControlDay *sunday = &aControlTable[6]; - currentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1]; - } - /* - ESP_LOGI(TAG, "Active entry found - Time: %02d:%02d, " - "Return Temp: %lf, Chamber Temp: %lf", - currentControlEntry.timestamp.hour, - currentControlEntry.timestamp.minute, - currentControlEntry.fReturnFlowTemperature, - currentControlEntry.fChamberTemperature); - */ - return; } } + + // If we reached here, current time is after all entries this week + // Use the last entry (Sunday evening) + const sControlDay *sunday = &gControlTable[6]; + gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1]; + + // ESP_LOGI(TAG, "Using last entry of week - Time: %02d:%02d", gCurrentControlEntry.timestamp.hour, gCurrentControlEntry.timestamp.minute); + xSemaphoreGiveRecursive(xMutexAccessControl); + } + else + { + ESP_LOGE(TAG, "Unable to take mutex: findControlCurrentTemperatureEntry()"); } - - // If we reached here, current time is after all entries this week - // Use the last entry (Sunday evening) - const sControlDay *sunday = &aControlTable[6]; - currentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1]; - - // ESP_LOGI(TAG, "Using last entry of week - Time: %02d:%02d", currentControlEntry.timestamp.hour, currentControlEntry.timestamp.minute); } sControlTemperatureEntry getControlCurrentTemperatureEntry(void) { - return currentControlEntry; + sControlTemperatureEntry ret = gControlTable[0].aTemperatureEntries[0]; + if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE) + { + ret = gCurrentControlEntry; + xSemaphoreGiveRecursive(xMutexAccessControl); + } + else + { + ESP_LOGE(TAG, "Unable to take mutex: getControlCurrentTemperatureEntry()"); + } + + return ret; } diff --git a/main/inputs.c b/main/inputs.c index a9ef723..8df4fb8 100644 --- a/main/inputs.c +++ b/main/inputs.c @@ -49,7 +49,12 @@ void initInputs(void) .intr_type = GPIO_INTR_DISABLE // Disable interrupts }; - gpio_config(&ioConfBurnerFault); + esp_err_t ret = gpio_config(&ioConfBurnerFault); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); + return; + } xMutexAccessInputs = xSemaphoreCreateRecursiveMutex(); if (xMutexAccessInputs == NULL) @@ -94,17 +99,17 @@ void initMeasurement(sMeasurement *pMeasurement) pMeasurement->average10s.fValue = INITIALISATION_VALUE; pMeasurement->average10s.bufferCount = 0U; pMeasurement->average10s.bufferIndex = 0U; - memset(pMeasurement->average10s.samples, 0U, AVG10S_SAMPLE_SIZE); + memset(pMeasurement->average10s.samples, 0U, sizeof(float) * AVG10S_SAMPLE_SIZE); pMeasurement->average60s.fValue = INITIALISATION_VALUE; pMeasurement->average60s.bufferCount = 0U; pMeasurement->average60s.bufferIndex = 0U; - memset(pMeasurement->average60s.samples, 0U, AVG60S_SAMPLE_SIZE); + memset(pMeasurement->average60s.samples, 0U, sizeof(float) * AVG60S_SAMPLE_SIZE); pMeasurement->predict60s.fValue = INITIALISATION_VALUE; pMeasurement->predict60s.bufferCount = 0U; pMeasurement->predict60s.bufferIndex = 0U; - memset(pMeasurement->predict60s.samples, 0U, PRED60S_SAMPLE_SIZE); + memset(pMeasurement->predict60s.samples, 0U, sizeof(float) * PRED60S_SAMPLE_SIZE); } void updateAverage(sMeasurement *pMeasurement) @@ -122,12 +127,19 @@ void updateAverage(sMeasurement *pMeasurement) } float sum = 0.0; - for (int i = 0; i <= pMeasurement->average10s.bufferCount; i++) + for (int i = 0; i < pMeasurement->average10s.bufferCount; i++) { sum += pMeasurement->average10s.samples[i]; } - pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount; + if (pMeasurement->average10s.bufferCount == 0U) + { + pMeasurement->average10s.fValue = 0.0f; + } + else + { + pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount; + } // Average form the last 60sec pMeasurement->average60s.samples[pMeasurement->average60s.bufferIndex] = pMeasurement->fCurrentValue; @@ -144,7 +156,14 @@ void updateAverage(sMeasurement *pMeasurement) sum += pMeasurement->average60s.samples[i]; } - pMeasurement->average60s.fValue = sum / pMeasurement->average60s.bufferCount; + if (pMeasurement->average60s.bufferCount == 0U) + { + pMeasurement->average60s.fValue = 0.0f; + } + else + { + pMeasurement->average60s.fValue = sum / pMeasurement->average60s.bufferCount; + } // Damped current value if (pMeasurement->fDampedValue == INITIALISATION_VALUE) diff --git a/main/metrics.c b/main/metrics.c index 6f1b2db..5c10247 100644 --- a/main/metrics.c +++ b/main/metrics.c @@ -301,23 +301,23 @@ void taskMetrics(void *pvParameters) // Wifi RSSI wifi_ap_record_t ap; - esp_wifi_sta_get_ap_info(&ap); + ap.rssi = 0U; + ESP_ERROR_CHECK(esp_wifi_sta_get_ap_info(&ap)); strcpy(aMetrics[u16MetricCounter].caMetricName, "wifi_rssi"); aMetrics[u16MetricCounter].type = INTEGER_64; aMetrics[u16MetricCounter].i64MetricValue = ap.rssi; u16MetricCounter++; - ESP_ERROR_CHECK(u16MetricCounter > METRIC_MAX_COUNT); + configASSERT(!(u16MetricCounter > METRIC_MAX_COUNT)); vSetMetrics(aMetrics, u16MetricCounter); } } void vSetMetrics(sMetric *paMetrics, uint16_t u16Size) { - if (xSemaphoreTakeRecursive(xMutexAccessMetricResponse, pdMS_TO_TICKS(5000)) == pdTRUE) { - memset(caHtmlResponse, 0U, strlen(caHtmlResponse)); + memset(caHtmlResponse, 0U, HTML_RESPONSE_SIZE); for (uint16_t u16Index = 0U; u16Index < u16Size; u16Index++) { char caValueBuffer[64]; diff --git a/main/outputs.c b/main/outputs.c index 7536e76..9861b60 100644 --- a/main/outputs.c +++ b/main/outputs.c @@ -41,9 +41,26 @@ void initOutputs(void) .intr_type = GPIO_INTR_DISABLE // Disable interrupts }; - gpio_config(&ioConfCirculationPump); - gpio_config(&ioConfBurner); - gpio_config(&ioConfSafetyContact); + esp_err_t ret = gpio_config(&ioConfCirculationPump); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); + return; + } + + ret = gpio_config(&ioConfBurner); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); + return; + } + + ret = gpio_config(&ioConfSafetyContact); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); + return; + } xMutexAccessOutputs = xSemaphoreCreateRecursiveMutex(); if (xMutexAccessOutputs == NULL) @@ -55,7 +72,17 @@ void initOutputs(void) eOutput getCirculationPumpState(void) { - return sCirculationPumpState; + eOutput ret = ENABLED; + if (xSemaphoreTakeRecursive(xMutexAccessOutputs, pdMS_TO_TICKS(5000)) == pdTRUE) + { + ret = sCirculationPumpState; + xSemaphoreGiveRecursive(xMutexAccessOutputs); + } + else + { + ESP_LOGE(TAG, "Unable to take mutex: getCirculationPumpState()"); + } + return ret; } void setCirculationPumpState(eOutput in) @@ -70,6 +97,7 @@ void setCirculationPumpState(eOutput in) break; case DISABLED: gpio_set_level(uCirculationPumpGpioPin, 1U); // Switch off Circulation Pump + break; default: break; } @@ -108,6 +136,7 @@ void setBurnerState(eOutput in) break; case DISABLED: gpio_set_level(uBurnerGpioPin, 1U); // Switch off Burner + break; default: break; } @@ -146,6 +175,7 @@ void setSafetyControlState(eOutput in) break; case DISABLED: gpio_set_level(uSafetyContactGpioPin, 1U); // Switch off power for Burner + break; default: break; } diff --git a/main/safety.c b/main/safety.c index db60a87..f592a86 100644 --- a/main/safety.c +++ b/main/safety.c @@ -2,10 +2,12 @@ #include "freertos/task.h" #include "esp_log.h" #include +#include #include "safety.h" #define PERIODIC_INTERVAL 1U // run safety checks every 1sec #define SENSOR_GRACE_PERIOD (60U * 30U) // period that a sensor can report the same reading in seconds +#define FLOAT_EPSILON 0.0001f static const char *TAG = "smart-oil-heater-control-system-safety"; static SemaphoreHandle_t xMutexAccessSafety = NULL; @@ -91,7 +93,7 @@ void checkSensorSanity(void) } else { - if (sCurrentMeasurement.fCurrentValue == sanityChecks[i].fSensorTemperatureLast) + if (fabsf(sCurrentMeasurement.fCurrentValue - sanityChecks[i].fSensorTemperatureLast) < FLOAT_EPSILON) { sanityChecks[i].uUnchangedCounter++; if (sanityChecks[i].uUnchangedCounter >= (SENSOR_GRACE_PERIOD / PERIODIC_INTERVAL)) @@ -103,6 +105,7 @@ void checkSensorSanity(void) } else { + sanityChecks[i].uUnchangedCounter = 0U; sanityChecks[i].fSensorTemperatureLast = sCurrentMeasurement.fCurrentValue; if (sCurrentMeasurement.fCurrentValue > sanityChecks[i].sSensorLimit.max) @@ -119,12 +122,10 @@ void checkSensorSanity(void) } else { - sanityChecks[i].uUnchangedCounter = 0U; sanityChecks[i].state = SENSOR_NO_ERROR; } } } - // printf(" state: %u\n", sanityChecks[i].state); } } @@ -143,7 +144,7 @@ void getSensorSanityStates(sSensorSanityCheck *pSensorSanityChecks) { // Copy only the needed attributes pSensorSanityChecks[i].state = sanityChecks[i].state; - strcpy(pSensorSanityChecks[i].name, sanityChecks[i].name); + strncpy(pSensorSanityChecks[i].name, sanityChecks[i].name, MAX_ERROR_STRING_SIZE); } xSemaphoreGiveRecursive(xMutexAccessSafety); } diff --git a/main/sntp.c b/main/sntp.c index 638bfa4..fab2d6f 100644 --- a/main/sntp.c +++ b/main/sntp.c @@ -6,7 +6,7 @@ #include "sntp.h" static const char *TAG = "smart-oil-heater-control-system-sntp"; -static eSntpState sntpState = SYNC_NOT_STARTED; +static volatile eSntpState sntpState = SYNC_NOT_STARTED; void time_sync_notification_cb(struct timeval *tv); void initSntp(void) diff --git a/main/wifi.c b/main/wifi.c index 05bc4c8..063d158 100644 --- a/main/wifi.c +++ b/main/wifi.c @@ -29,16 +29,22 @@ static bool parse_bssid(const char *bssid_str, uint8_t *bssid); void initWifi(void) { s_wifi_event_group = xEventGroupCreate(); + if (s_wifi_event_group == NULL) + { + ESP_LOGE(TAG, "xEventGroupCreate() failed!"); + return; + } + ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_t *my_sta = esp_netif_create_default_wifi_sta(); - esp_netif_dhcpc_stop(my_sta); + ESP_ERROR_CHECK(esp_netif_dhcpc_stop(my_sta)); esp_netif_ip_info_t ip_info; ip_info.ip.addr = ipaddr_addr(CONFIG_STATIC_IP_ADDR); ip_info.gw.addr = ipaddr_addr(CONFIG_STATIC_GATEWAY_IP_ADDR); ip_info.netmask.addr = ipaddr_addr(CONFIG_STATIC_IP_NETMASK); - esp_netif_set_ip_info(my_sta, &ip_info); + ESP_ERROR_CHECK(esp_netif_set_ip_info(my_sta, &ip_info)); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); From 085f5b4acbdbf73f2bdda34182362b9fb58f92473651e7d7d5bcce660b3b00b3 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sun, 10 May 2026 11:12:18 +0200 Subject: [PATCH 6/6] Relocate the configuration values (#29) closes https://git.mosad.xyz/localhorst/smart-oil-heating-control-system/issues/24 Reviewed-on: https://git.mosad.xyz/localhorst/smart-oil-heating-control-system/pulls/29 Co-authored-by: localhorst Co-committed-by: localhorst --- main/Kconfig.projbuild | 419 +++++++++++++++++++++++++++++++++++++---- main/control.c | 51 ++--- main/control.h | 12 ++ main/inputs.c | 21 ++- main/inputs.h | 10 +- main/main.c | 10 +- main/metrics.c | 19 +- main/outputs.c | 11 +- main/safety.c | 17 +- main/safety.h | 13 ++ main/sntp.c | 9 +- main/wifi.c | 7 +- 12 files changed, 488 insertions(+), 111 deletions(-) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 5b17d67..5c0bcff 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -1,36 +1,391 @@ menu "Smart Oil Heating Control System" - config SSID - string "SSID" - default "my WiFi SSID" - config WIFI_PASSWORD - string "WIFI_PASSWORD" - default "my WIFI Password" - config STATIC_IP_ADDR - string "Static IPv4 address" - default "192.168.0.42" - config STATIC_IP_NETMASK - string "Static IPv4 netmask" - default "255.255.0.0" - config STATIC_GATEWAY_IP_ADDR - string "Static IPv4 gateway address" - default "192.168.0.1" - config SNTP_SERVER_IP_ADDR - string "SNTP IPv4 server address" - default "192.168.0.1" - config ENV_WIFI_BSSID_LOCK - bool "Lock to specific Access Point (BSSID)" - default n - help - When enabled, the device will only connect to the access point - with the specified MAC address (BSSID). Useful when multiple APs - share the same SSID. - config ENV_WIFI_BSSID - string "Access Point MAC Address (BSSID)" - default "00:00:00:00:00:00" - depends on ENV_WIFI_BSSID_LOCK - help - MAC address of the access point to connect to. - Format: XX:XX:XX:XX:XX:XX (uppercase or lowercase) + menu "WiFi Configuration" + config SSID + string "WiFi SSID" + default "my WiFi SSID" + help + The SSID of the WiFi network to connect to. + + config WIFI_PASSWORD + string "WiFi Password" + default "my WIFI Password" + help + The password for the WiFi network. + + config STATIC_IP_ADDR + string "Static IPv4 address" + default "192.168.0.42" + help + Static IP address for the ESP32. + + config STATIC_IP_NETMASK + string "Static IPv4 netmask" + default "255.255.0.0" + help + Network mask for the static IP configuration. + + config STATIC_GATEWAY_IP_ADDR + string "Static IPv4 gateway address" + default "192.168.0.1" + help + Gateway IP address for network routing. + + config SNTP_SERVER_IP_ADDR + string "SNTP server address" + default "192.168.0.1" + help + NTP server address for time synchronization. + endmenu + + menu "GPIO Configuration" + menu "Input GPIOs" + config GPIO_BURNER_FAULT + int "Burner fault input GPIO" + range 0 39 + default 19 + help + GPIO pin connected to the burner fault signal. + + config GPIO_DS18B20_ONEWIRE + int "DS18B20 1-Wire bus GPIO" + range 0 39 + default 4 + help + GPIO pin for the 1-Wire bus (DS18B20 temperature sensors). + endmenu + + menu "Output GPIOs" + config GPIO_CIRCULATION_PUMP + int "Circulation pump output GPIO" + range 0 39 + default 27 + help + GPIO pin to control the circulation pump relay. + + config GPIO_BURNER + int "Burner control output GPIO" + range 0 39 + default 14 + help + GPIO pin to control the burner relay. + + config GPIO_SAFETY_CONTACT + int "Safety contact output GPIO" + range 0 39 + default 12 + help + GPIO pin for the safety contact relay (main power to burner). + endmenu + endmenu + + menu "1-Wire Sensor Addresses" + config ONEWIRE_ADDR_CHAMBER_TEMP + hex "Chamber temperature sensor address" + default 0xd00000108cd01d28 + help + 64-bit 1-Wire address of the chamber temperature sensor. + + config ONEWIRE_ADDR_OUTDOOR_TEMP + hex "Outdoor temperature sensor address" + default 0xd70000108a9b9128 + help + 64-bit 1-Wire address of the outdoor temperature sensor. + + config ONEWIRE_ADDR_INLET_FLOW_TEMP + hex "Inlet flow temperature sensor address" + default 0x410000108b8c0628 + help + 64-bit 1-Wire address of the inlet flow temperature sensor. + + config ONEWIRE_ADDR_RETURN_FLOW_TEMP + hex "Return flow temperature sensor address" + default 0x90000108cc77c28 + help + 64-bit 1-Wire address of the return flow temperature sensor. + endmenu + + menu "Temperature Control Settings" + menu "Target Temperatures" + config TEMP_RETURN_FLOW_LOWER_LIMIT_DAY + int "Return flow lower limit (day) [°C x 10]" + range 150 500 + default 300 + help + Minimum return flow temperature during day mode in 0.1°C units. + Example: 300 = 30.0°C + + config TEMP_RETURN_FLOW_LOWER_LIMIT_NIGHT + int "Return flow lower limit (night) [°C x 10]" + range 150 500 + default 250 + help + Minimum return flow temperature during night mode in 0.1°C units. + Example: 250 = 25.0°C + + config TEMP_CHAMBER_TARGET + int "Chamber target temperature [°C x 10]" + range 500 950 + default 800 + help + Maximum chamber temperature target in 0.1°C units. + Example: 800 = 80.0°C + + config TEMP_CHAMBER_THRESHOLD + int "Chamber temperature threshold [°C x 10]" + range 300 700 + default 450 + help + Minimum chamber temperature to enable burner in 0.1°C units. + Example: 450 = 45.0°C + + config TEMP_CIRCULATION_PUMP_THRESHOLD + int "Circulation pump threshold [°C x 10]" + range 200 500 + default 300 + help + Minimum chamber temperature to enable circulation pump in 0.1°C units. + Example: 300 = 30.0°C + endmenu + + menu "Summer Mode Settings" + config TEMP_SUMMER_MODE_HIGH + int "Summer mode activation threshold [°C x 10]" + range 150 300 + default 200 + help + Outdoor temperature above which summer mode activates in 0.1°C units. + Example: 200 = 20.0°C + + config TEMP_SUMMER_MODE_LOW + int "Summer mode deactivation threshold [°C x 10]" + range 100 250 + default 150 + help + Outdoor temperature below which summer mode deactivates in 0.1°C units. + Example: 150 = 15.0°C + endmenu + + config BURNER_FAULT_DETECTION_SECONDS + int "Burner fault detection timeout (seconds)" + range 60 600 + default 240 + help + Time in seconds to wait before checking for burner fault after enabling. + endmenu + + menu "Sensor Limits" + menu "Chamber Temperature Limits" + config SENSOR_LIMIT_CHAMBER_MAX + int "Chamber sensor maximum [°C x 10]" + range 500 1200 + default 950 + help + Maximum valid chamber temperature reading in 0.1°C units. + + config SENSOR_LIMIT_CHAMBER_MIN + int "Chamber sensor minimum [°C x 10]" + range -400 100 + default -100 + help + Minimum valid chamber temperature reading in 0.1°C units. + endmenu + + menu "Outdoor Temperature Limits" + config SENSOR_LIMIT_OUTDOOR_MAX + int "Outdoor sensor maximum [°C x 10]" + range 300 600 + default 450 + help + Maximum valid outdoor temperature reading in 0.1°C units. + + config SENSOR_LIMIT_OUTDOOR_MIN + int "Outdoor sensor minimum [°C x 10]" + range -500 0 + default -200 + help + Minimum valid outdoor temperature reading in 0.1°C units. + endmenu + + menu "Inlet Flow Temperature Limits" + config SENSOR_LIMIT_INLET_MAX + int "Inlet flow sensor maximum [°C x 10]" + range 500 1200 + default 950 + help + Maximum valid inlet flow temperature reading in 0.1°C units. + + config SENSOR_LIMIT_INLET_MIN + int "Inlet flow sensor minimum [°C x 10]" + range -400 100 + default -100 + help + Minimum valid inlet flow temperature reading in 0.1°C units. + endmenu + + menu "Return Flow Temperature Limits" + config SENSOR_LIMIT_RETURN_MAX + int "Return flow sensor maximum [°C x 10]" + range 500 1200 + default 950 + help + Maximum valid return flow temperature reading in 0.1°C units. + + config SENSOR_LIMIT_RETURN_MIN + int "Return flow sensor minimum [°C x 10]" + range -400 100 + default -100 + help + Minimum valid return flow temperature reading in 0.1°C units. + endmenu + + config SENSOR_GRACE_PERIOD_MINUTES + int "Sensor unchanged grace period (minutes)" + range 1 120 + default 30 + help + Maximum time in minutes a sensor can report unchanged values + before being flagged as faulty. + endmenu + + menu "Damping Factors" + config DAMPING_FACTOR_WARMER + int "Damping factor warmer [x 0.00001]" + range 1 100 + default 1 + help + Damping factor for rising temperatures in units of 0.00001. + Example: 1 = 0.00001 (0.001%) + + config DAMPING_FACTOR_COLDER + int "Damping factor colder [x 0.00001]" + range 1 100 + default 5 + help + Damping factor for falling temperatures in units of 0.00001. + Example: 5 = 0.00005 (0.005%) + endmenu + + menu "Heating Schedule" + menu "Weekday Schedule (Monday-Thursday)" + config SCHEDULE_WEEKDAY_DAY_START_HOUR + int "Day mode start hour" + range 0 23 + default 4 + help + Hour when day mode starts on weekdays (24h format). + + config SCHEDULE_WEEKDAY_DAY_START_MINUTE + int "Day mode start minute" + range 0 59 + default 45 + help + Minute when day mode starts on weekdays. + + config SCHEDULE_WEEKDAY_NIGHT_START_HOUR + int "Night mode start hour" + range 0 23 + default 22 + help + Hour when night mode starts on weekdays (24h format). + + config SCHEDULE_WEEKDAY_NIGHT_START_MINUTE + int "Night mode start minute" + range 0 59 + default 0 + help + Minute when night mode starts on weekdays. + endmenu + + menu "Friday Schedule" + config SCHEDULE_FRIDAY_DAY_START_HOUR + int "Day mode start hour" + range 0 23 + default 4 + help + Hour when day mode starts on Friday (24h format). + + config SCHEDULE_FRIDAY_DAY_START_MINUTE + int "Day mode start minute" + range 0 59 + default 45 + help + Minute when day mode starts on Friday. + + config SCHEDULE_FRIDAY_NIGHT_START_HOUR + int "Night mode start hour" + range 0 23 + default 23 + help + Hour when night mode starts on Friday (24h format). + + config SCHEDULE_FRIDAY_NIGHT_START_MINUTE + int "Night mode start minute" + range 0 59 + default 0 + help + Minute when night mode starts on Friday. + endmenu + + menu "Saturday Schedule" + config SCHEDULE_SATURDAY_DAY_START_HOUR + int "Day mode start hour" + range 0 23 + default 6 + help + Hour when day mode starts on Saturday (24h format). + + config SCHEDULE_SATURDAY_DAY_START_MINUTE + int "Day mode start minute" + range 0 59 + default 45 + help + Minute when day mode starts on Saturday. + + config SCHEDULE_SATURDAY_NIGHT_START_HOUR + int "Night mode start hour" + range 0 23 + default 23 + help + Hour when night mode starts on Saturday (24h format). + + config SCHEDULE_SATURDAY_NIGHT_START_MINUTE + int "Night mode start minute" + range 0 59 + default 30 + help + Minute when night mode starts on Saturday. + endmenu + + menu "Sunday Schedule" + config SCHEDULE_SUNDAY_DAY_START_HOUR + int "Day mode start hour" + range 0 23 + default 6 + help + Hour when day mode starts on Sunday (24h format). + + config SCHEDULE_SUNDAY_DAY_START_MINUTE + int "Day mode start minute" + range 0 59 + default 45 + help + Minute when day mode starts on Sunday. + + config SCHEDULE_SUNDAY_NIGHT_START_HOUR + int "Night mode start hour" + range 0 23 + default 22 + help + Hour when night mode starts on Sunday (24h format). + + config SCHEDULE_SUNDAY_NIGHT_START_MINUTE + int "Night mode start minute" + range 0 59 + default 30 + help + Minute when night mode starts on Sunday. + endmenu + endmenu endmenu diff --git a/main/control.c b/main/control.c index 32bc58a..9280c71 100644 --- a/main/control.c +++ b/main/control.c @@ -1,28 +1,15 @@ #include "control.h" -#include "esp_log.h" -#include "esp_timer.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" #include "inputs.h" #include "outputs.h" #include "safety.h" #include "sntp.h" -#define PERIODIC_INTERVAL 1U // Run control loop every 1 second +#include "esp_log.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" -// Temperature thresholds -#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY 30.0f -#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT 25.0f -#define CHAMBER_TEMPERATURE_TARGET 80.0f // Max cutoff temperature -#define CHAMBER_TEMPERATURE_THRESHOLD 45.0f // Min threshold for burner enable -#define SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH \ - 20.0f // Summer mode will be activated -#define SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW \ - 15.0f // Summer mode will be deactivated --> Heating starts -#define CIRCULATION_PUMP_TEMPERATURE_THRESHOLD \ - 30.0f // Min threshold of chamber for circulation pump enable -#define BURNER_FAULT_DETECTION_THRESHOLD \ - (60U * 4U) // Burner fault detection after 4 minutes +#define PERIODIC_INTERVAL 1U // Run control loop every 1 second static const char *TAG = "smart-oil-heater-control-system-control"; static eControlState gControlState = CONTROL_STARTING; @@ -30,58 +17,58 @@ static eControlState gControlState = CONTROL_STARTING; static const sControlDay gControlTable[] = { {MONDAY, 2U, - {{{4, 45}, + {{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMBER_TEMPERATURE_TARGET}, - {{22, 0}, + {{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMBER_TEMPERATURE_TARGET}}}, {TUESDAY, 2U, - {{{4, 45}, + {{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMBER_TEMPERATURE_TARGET}, - {{22, 0}, + {{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMBER_TEMPERATURE_TARGET}}}, {WEDNESDAY, 2U, - {{{4, 45}, + {{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMBER_TEMPERATURE_TARGET}, - {{22, 0}, + {{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMBER_TEMPERATURE_TARGET}}}, {THURSDAY, 2U, - {{{4, 45}, + {{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMBER_TEMPERATURE_TARGET}, - {{22, 0}, + {{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMBER_TEMPERATURE_TARGET}}}, {FRIDAY, 2U, - {{{4, 45}, + {{{CONFIG_SCHEDULE_FRIDAY_DAY_START_HOUR, CONFIG_SCHEDULE_FRIDAY_DAY_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMBER_TEMPERATURE_TARGET}, - {{23, 0}, + {{CONFIG_SCHEDULE_FRIDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_FRIDAY_NIGHT_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMBER_TEMPERATURE_TARGET}}}, {SATURDAY, 2U, - {{{6, 45}, + {{{CONFIG_SCHEDULE_SATURDAY_DAY_START_HOUR, CONFIG_SCHEDULE_SATURDAY_DAY_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMBER_TEMPERATURE_TARGET}, - {{23, 30}, + {{CONFIG_SCHEDULE_SATURDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_SATURDAY_NIGHT_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMBER_TEMPERATURE_TARGET}}}, {SUNDAY, 2U, - {{{6, 45}, + {{{CONFIG_SCHEDULE_SUNDAY_DAY_START_HOUR, CONFIG_SCHEDULE_SUNDAY_DAY_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMBER_TEMPERATURE_TARGET}, - {{22, 30}, + {{CONFIG_SCHEDULE_SUNDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_SUNDAY_NIGHT_START_MINUTE}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMBER_TEMPERATURE_TARGET}}}, }; diff --git a/main/control.h b/main/control.h index 7690bd1..0096038 100644 --- a/main/control.h +++ b/main/control.h @@ -1,8 +1,20 @@ #pragma once + +#include "sdkconfig.h" + #include #define MAX_TEMPERATURE_ENTRIES_PER_DAY 24U +#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY (CONFIG_TEMP_RETURN_FLOW_LOWER_LIMIT_DAY / 10.0f) +#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT (CONFIG_TEMP_RETURN_FLOW_LOWER_LIMIT_NIGHT / 10.0f) +#define CHAMBER_TEMPERATURE_TARGET (CONFIG_TEMP_CHAMBER_TARGET / 10.0f) +#define CHAMBER_TEMPERATURE_THRESHOLD (CONFIG_TEMP_CHAMBER_THRESHOLD / 10.0f) +#define SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH (CONFIG_TEMP_SUMMER_MODE_HIGH / 10.0f) +#define SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW (CONFIG_TEMP_SUMMER_MODE_LOW / 10.0f) +#define CIRCULATION_PUMP_TEMPERATURE_THRESHOLD (CONFIG_TEMP_CIRCULATION_PUMP_THRESHOLD / 10.0f) +#define BURNER_FAULT_DETECTION_THRESHOLD CONFIG_BURNER_FAULT_DETECTION_SECONDS + typedef enum _ControlState { CONTROL_STARTING, diff --git a/main/inputs.c b/main/inputs.c index 8df4fb8..56c04b2 100644 --- a/main/inputs.c +++ b/main/inputs.c @@ -1,25 +1,26 @@ +#include "inputs.h" + #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" -#include -#include #include "esp_log.h" #include -#include "inputs.h" +#include +#include #define MAX_DN18B20_SENSORS 4U #define ONE_WIRE_LOOPS 4U // try to read the 1-Wire sensors that often #define PERIODIC_INTERVAL 1U // read and compute the inputs every 1sec static const char *TAG = "smart-oil-heater-control-system-inputs"; -const uint8_t uBurnerFaultPin = 19U; -const uint8_t uDS18B20Pin = 4U; +const uint8_t uBurnerFaultPin = CONFIG_GPIO_BURNER_FAULT; +const uint8_t uDS18B20Pin = CONFIG_GPIO_DS18B20_ONEWIRE; -const onewire_addr_t uChamperTempSensorAddr = 0xd00000108cd01d28; -const onewire_addr_t uOutdoorTempSensorAddr = 0xd70000108a9b9128; -const onewire_addr_t uInletFlowTempSensorAddr = 0x410000108b8c0628; -const onewire_addr_t uReturnFlowTempSensorAddr = 0x90000108cc77c28; +const onewire_addr_t uChamperTempSensorAddr = CONFIG_ONEWIRE_ADDR_CHAMBER_TEMP; +const onewire_addr_t uOutdoorTempSensorAddr = CONFIG_ONEWIRE_ADDR_OUTDOOR_TEMP; +const onewire_addr_t uInletFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_INLET_FLOW_TEMP; +const onewire_addr_t uReturnFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_RETURN_FLOW_TEMP; onewire_addr_t uOneWireAddresses[MAX_DN18B20_SENSORS]; float fDS18B20Temps[MAX_DN18B20_SENSORS]; @@ -416,4 +417,4 @@ eBurnerErrorState getBurnerError(void) ESP_LOGE(TAG, "Unable to take mutex: getBurnerError()"); } return ret; -} \ No newline at end of file +} diff --git a/main/inputs.h b/main/inputs.h index 17e095b..be68658 100644 --- a/main/inputs.h +++ b/main/inputs.h @@ -1,13 +1,17 @@ #pragma once +#include "sdkconfig.h" + +#include + #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define INITIALISATION_VALUE 0.0f #define AVG10S_SAMPLE_SIZE 10U #define AVG60S_SAMPLE_SIZE 60U #define AVG24H_SAMPLE_SIZE 24U #define PRED60S_SAMPLE_SIZE 60U -#define DAMPING_FACTOR_WARMER 0.00001f // 0.001% -#define DAMPING_FACTOR_COLDER 0.00005f // 0.005% +#define DAMPING_FACTOR_WARMER (CONFIG_DAMPING_FACTOR_WARMER * 0.00001f) +#define DAMPING_FACTOR_COLDER (CONFIG_DAMPING_FACTOR_COLDER * 0.00001f) typedef enum _BurnerErrorState { @@ -52,4 +56,4 @@ sMeasurement getChamberTemperature(void); sMeasurement getOutdoorTemperature(void); sMeasurement getInletFlowTemperature(void); sMeasurement getReturnFlowTemperature(void); -eBurnerErrorState getBurnerError(void); \ No newline at end of file +eBurnerErrorState getBurnerError(void); diff --git a/main/main.c b/main/main.c index 82c2d63..622cd51 100644 --- a/main/main.c +++ b/main/main.c @@ -1,7 +1,3 @@ -#include "esp_log.h" -#include -#include "nvs_flash.h" - #include "safety.h" #include "metrics.h" #include "outputs.h" @@ -10,6 +6,10 @@ #include "wifi.h" #include "sntp.h" +#include "esp_log.h" +#include "esp_system.h" +#include "nvs_flash.h" + static const char *TAG = "smart-oil-heater-control-system"; void app_main(void) @@ -39,4 +39,4 @@ void app_main(void) vTaskDelay(pdMS_TO_TICKS(1000)); // Do nothing ;-) } -} \ No newline at end of file +} diff --git a/main/metrics.c b/main/metrics.c index 5c10247..ba3c436 100644 --- a/main/metrics.c +++ b/main/metrics.c @@ -1,12 +1,3 @@ -#include -#include "esp_timer.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_wifi.h" -#include "esp_log.h" -#include -#include - #include "metrics.h" #include "outputs.h" #include "inputs.h" @@ -14,6 +5,16 @@ #include "sntp.h" #include "control.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_wifi.h" +#include "esp_log.h" + +#include +#include +#include + static const char *TAG = "smart-oil-heater-control-system-metrics"; char caHtmlResponse[HTML_RESPONSE_SIZE]; diff --git a/main/outputs.c b/main/outputs.c index 9861b60..63dd0e6 100644 --- a/main/outputs.c +++ b/main/outputs.c @@ -1,14 +1,15 @@ +#include "outputs.h" + +#include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" #include "esp_log.h" -#include "outputs.h" - static const char *TAG = "smart-oil-heater-control-system-outputs"; -const uint8_t uCirculationPumpGpioPin = 27U; -const uint8_t uBurnerGpioPin = 14U; -const uint8_t uSafetyContactGpioPin = 12U; +const uint8_t uCirculationPumpGpioPin = CONFIG_GPIO_CIRCULATION_PUMP; +const uint8_t uBurnerGpioPin = CONFIG_GPIO_BURNER; +const uint8_t uSafetyContactGpioPin = CONFIG_GPIO_SAFETY_CONTACT; static SemaphoreHandle_t xMutexAccessOutputs = NULL; static eOutput sCirculationPumpState; diff --git a/main/safety.c b/main/safety.c index f592a86..7fb6ae4 100644 --- a/main/safety.c +++ b/main/safety.c @@ -1,21 +1,22 @@ +#include "safety.h" + #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" + #include #include -#include "safety.h" -#define PERIODIC_INTERVAL 1U // run safety checks every 1sec -#define SENSOR_GRACE_PERIOD (60U * 30U) // period that a sensor can report the same reading in seconds +#define PERIODIC_INTERVAL 1U // run safety checks every 1sec +#define SENSOR_GRACE_PERIOD (CONFIG_SENSOR_GRACE_PERIOD_MINUTES * 60U) // period that a sensor can report the same reading in seconds #define FLOAT_EPSILON 0.0001f - static const char *TAG = "smart-oil-heater-control-system-safety"; static SemaphoreHandle_t xMutexAccessSafety = NULL; static sSensorSanityCheck sanityChecks[NUMBER_OF_SENSOR_SANITY_CHECKS] = { - {SENSOR_NO_ERROR, "chamber_temperature", {95.0f, -10.0f}, 0.0f, 0U, getChamberTemperature}, - {SENSOR_NO_ERROR, "outdoor_temperature", {45.0f, -20.0f}, 0.0f, 0U, getOutdoorTemperature}, - {SENSOR_NO_ERROR, "inlet_flow_temperature", {95.0f, -10.0f}, 0.0f, 0U, getInletFlowTemperature}, - {SENSOR_NO_ERROR, "return_flow_temperature", {95.0f, -10.0f}, 0.0f, 0U, getReturnFlowTemperature}}; + {SENSOR_NO_ERROR, "chamber_temperature", {SENSOR_LIMIT_CHAMBER_MAX, SENSOR_LIMIT_CHAMBER_MIN}, 0.0f, 0U, getChamberTemperature}, + {SENSOR_NO_ERROR, "outdoor_temperature", {SENSOR_LIMIT_OUTDOOR_MAX, SENSOR_LIMIT_OUTDOOR_MIN}, 0.0f, 0U, getOutdoorTemperature}, + {SENSOR_NO_ERROR, "inlet_flow_temperature", {SENSOR_LIMIT_INLET_MAX, SENSOR_LIMIT_INLET_MIN}, 0.0f, 0U, getInletFlowTemperature}, + {SENSOR_NO_ERROR, "return_flow_temperature", {SENSOR_LIMIT_RETURN_MAX, SENSOR_LIMIT_RETURN_MIN}, 0.0f, 0U, getReturnFlowTemperature}}; static eSafetyState sSafetyState = SAFETY_NO_ERROR; void taskSafety(void *pvParameters); diff --git a/main/safety.h b/main/safety.h index 825e7cd..58e6974 100644 --- a/main/safety.h +++ b/main/safety.h @@ -3,9 +3,22 @@ #include "outputs.h" #include "inputs.h" +#include "sdkconfig.h" + +#include + #define MAX_ERROR_STRING_SIZE 64U #define NUMBER_OF_SENSOR_SANITY_CHECKS 4U +#define SENSOR_LIMIT_CHAMBER_MAX (CONFIG_SENSOR_LIMIT_CHAMBER_MAX / 10.0f) +#define SENSOR_LIMIT_CHAMBER_MIN (CONFIG_SENSOR_LIMIT_CHAMBER_MIN / 10.0f) +#define SENSOR_LIMIT_OUTDOOR_MAX (CONFIG_SENSOR_LIMIT_OUTDOOR_MAX / 10.0f) +#define SENSOR_LIMIT_OUTDOOR_MIN (CONFIG_SENSOR_LIMIT_OUTDOOR_MIN / 10.0f) +#define SENSOR_LIMIT_INLET_MAX (CONFIG_SENSOR_LIMIT_INLET_MAX / 10.0f) +#define SENSOR_LIMIT_INLET_MIN (CONFIG_SENSOR_LIMIT_INLET_MIN / 10.0f) +#define SENSOR_LIMIT_RETURN_MAX (CONFIG_SENSOR_LIMIT_RETURN_MAX / 10.0f) +#define SENSOR_LIMIT_RETURN_MIN (CONFIG_SENSOR_LIMIT_RETURN_MIN / 10.0f) + typedef enum _SensorErrorState { SENSOR_NO_ERROR, diff --git a/main/sntp.c b/main/sntp.c index fab2d6f..f00bdd6 100644 --- a/main/sntp.c +++ b/main/sntp.c @@ -1,9 +1,10 @@ -#include -#include -#include +#include "sntp.h" + +#include "esp_sntp.h" #include "esp_log.h" -#include "sntp.h" +#include +#include static const char *TAG = "smart-oil-heater-control-system-sntp"; static volatile eSntpState sntpState = SYNC_NOT_STARTED; diff --git a/main/wifi.c b/main/wifi.c index 063d158..c4938c6 100644 --- a/main/wifi.c +++ b/main/wifi.c @@ -1,15 +1,16 @@ -#include +#include "wifi.h" + #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "freertos/event_groups.h" #include "esp_wifi.h" #include "esp_event.h" -#include "freertos/event_groups.h" #include "esp_log.h" #include "esp_netif.h" #include -#include "wifi.h" +#include #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1