From cd739857408b6bc9901cad44f52690f84ad77d3cd0943c2d3b648f2e72c840eb Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 10:54:32 +0100 Subject: [PATCH 01/21] Wrong memset size --- main/inputs.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main/inputs.c b/main/inputs.c index a9ef723..1b40193 100644 --- a/main/inputs.c +++ b/main/inputs.c @@ -94,17 +94,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) -- 2.50.1 From 0775fda0ca5330e4e897d4ddc98b0fc083b0586b506f5c3ff2b1d7738715f54f Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 10:55:15 +0100 Subject: [PATCH 02/21] Off-by-one error (buffer overread) --- main/inputs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/inputs.c b/main/inputs.c index 1b40193..ad6be27 100644 --- a/main/inputs.c +++ b/main/inputs.c @@ -122,7 +122,7 @@ 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]; } -- 2.50.1 From 09a3c3a22dfe22236ac26280ee896e955bd12f0eaf540daaf964690b51303278 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 10:58:12 +0100 Subject: [PATCH 03/21] Misuse of ESP_ERROR_CHECK --- main/metrics.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/metrics.c b/main/metrics.c index 6f1b2db..f87763a 100644 --- a/main/metrics.c +++ b/main/metrics.c @@ -307,7 +307,7 @@ void taskMetrics(void *pvParameters) aMetrics[u16MetricCounter].i64MetricValue = ap.rssi; u16MetricCounter++; - ESP_ERROR_CHECK(u16MetricCounter > METRIC_MAX_COUNT); + configASSERT(u16MetricCounter > METRIC_MAX_COUNT); vSetMetrics(aMetrics, u16MetricCounter); } } -- 2.50.1 From 781f9a14453d9c08c9d50959915e83dd038bcf1a20b0684bec0e92fbdb124e14 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:02:31 +0100 Subject: [PATCH 04/21] ncorrect memset with strlen --- main/metrics.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/metrics.c b/main/metrics.c index f87763a..2aabf7a 100644 --- a/main/metrics.c +++ b/main/metrics.c @@ -307,7 +307,7 @@ void taskMetrics(void *pvParameters) aMetrics[u16MetricCounter].i64MetricValue = ap.rssi; u16MetricCounter++; - configASSERT(u16MetricCounter > METRIC_MAX_COUNT); + configASSERT(!(u16MetricCounter > METRIC_MAX_COUNT)); vSetMetrics(aMetrics, u16MetricCounter); } } @@ -317,7 +317,7 @@ 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]; -- 2.50.1 From 267197ec209b28e9707e673ceee37806a7dd808ad7d8a063d5f231ac1dbddae6 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:06:10 +0100 Subject: [PATCH 05/21] Missing mutex protection --- main/outputs.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/main/outputs.c b/main/outputs.c index 7536e76..c96ff41 100644 --- a/main/outputs.c +++ b/main/outputs.c @@ -55,7 +55,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) -- 2.50.1 From 8c3dbc2886d0dd13f865fb52cf56659eca3759bf81c8d9c49850006a43b97394 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:31:34 +0100 Subject: [PATCH 06/21] Unprotected shared state access --- main/control.c | 187 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 123 insertions(+), 64 deletions(-) diff --git a/main/control.c b/main/control.c index 1098bc7..911421d 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 @@ -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) @@ -178,10 +186,10 @@ void taskControl(void *pvParameters) // 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)) { @@ -192,12 +200,12 @@ void taskControl(void *pvParameters) 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; @@ -224,7 +232,7 @@ void taskControl(void *pvParameters) // ESP_LOGW(TAG, "Burner fault detected: Disabling burner"); bHeatingInAction = false; eBurnerState = BURNER_FAULT; - sControlState = CONTROL_FAULT_BURNER; + setControlState(CONTROL_FAULT_BURNER); setBurnerState(DISABLED); setSafetyControlState(ENABLED); } @@ -253,7 +261,37 @@ 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) { @@ -294,66 +332,87 @@ 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); + */ + 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; } -- 2.50.1 From df3825df3a22efeea1e94159cdd310d492a910f2294bca4e925d79abce8ddb76 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:33:37 +0100 Subject: [PATCH 07/21] Non-thread-safe function --- main/control.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/control.c b/main/control.c index 911421d..955b624 100644 --- a/main/control.c +++ b/main/control.c @@ -295,13 +295,13 @@ eControlState getControlState(void) 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); } -- 2.50.1 From 10f9645580d890be641933b57ffdf5a9a5f2ed0d27680273d08e823000e8bf2b Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:39:37 +0100 Subject: [PATCH 08/21] Unchecked gpio_config returns --- main/inputs.c | 7 ++++++- main/outputs.c | 23 ++++++++++++++++++++--- main/sntp.c | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/main/inputs.c b/main/inputs.c index ad6be27..93b2adf 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) diff --git a/main/outputs.c b/main/outputs.c index c96ff41..6644413 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) 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) -- 2.50.1 From 67929580d5f430ba28279fb7b70ca9f506bedd3d840c09270c009c326bec0537 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:42:27 +0100 Subject: [PATCH 09/21] Unchecked xEventGroupCreate --- main/wifi.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main/wifi.c b/main/wifi.c index 6a4b806..29378ef 100644 --- a/main/wifi.c +++ b/main/wifi.c @@ -28,6 +28,12 @@ static void event_handler(void *arg, esp_event_base_t event_base, 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()); -- 2.50.1 From 020eb63e05b8851fcca304cff6ed8bf07dce16b0a4a3f63a703f68764b4ec579 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:43:26 +0100 Subject: [PATCH 10/21] Unchecked network configuration --- main/wifi.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/wifi.c b/main/wifi.c index 29378ef..2b628d9 100644 --- a/main/wifi.c +++ b/main/wifi.c @@ -38,12 +38,12 @@ void initWifi(void) 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)); -- 2.50.1 From 05757a503834b429979f26087099af85242df554e194fa06f96c9be2b67e3a62 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:45:49 +0100 Subject: [PATCH 11/21] Unchecked WiFi API call --- main/metrics.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/metrics.c b/main/metrics.c index 2aabf7a..5c10247 100644 --- a/main/metrics.c +++ b/main/metrics.c @@ -301,7 +301,8 @@ 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; @@ -314,7 +315,6 @@ void taskMetrics(void *pvParameters) void vSetMetrics(sMetric *paMetrics, uint16_t u16Size) { - if (xSemaphoreTakeRecursive(xMutexAccessMetricResponse, pdMS_TO_TICKS(5000)) == pdTRUE) { memset(caHtmlResponse, 0U, HTML_RESPONSE_SIZE); -- 2.50.1 From 0236ebcdd1cba119bc35299e32fa80960806cd6f7fb3f92efd9c1eee4a12aee9 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:47:04 +0100 Subject: [PATCH 12/21] Unsafe strcpy --- main/safety.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/safety.c b/main/safety.c index db60a87..53ca250 100644 --- a/main/safety.c +++ b/main/safety.c @@ -143,7 +143,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); } -- 2.50.1 From a9ec101bc61dc8e0d7fa6def51a43c25035e254deb3c050b04101583b30cb3f7 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:52:08 +0100 Subject: [PATCH 13/21] Floating-point equality comparison --- main/safety.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main/safety.c b/main/safety.c index 53ca250..94c0d04 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)) -- 2.50.1 From 40f757b7d1e1625dd2f0c80e9f3e433cc37a7d5bcba47e49efb464279154bd9b Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:54:18 +0100 Subject: [PATCH 14/21] uUnchangedCounter reset logic flaw --- main/safety.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main/safety.c b/main/safety.c index 94c0d04..f592a86 100644 --- a/main/safety.c +++ b/main/safety.c @@ -105,6 +105,7 @@ void checkSensorSanity(void) } else { + sanityChecks[i].uUnchangedCounter = 0U; sanityChecks[i].fSensorTemperatureLast = sCurrentMeasurement.fCurrentValue; if (sCurrentMeasurement.fCurrentValue > sanityChecks[i].sSensorLimit.max) @@ -121,12 +122,10 @@ void checkSensorSanity(void) } else { - sanityChecks[i].uUnchangedCounter = 0U; sanityChecks[i].state = SENSOR_NO_ERROR; } } } - // printf(" state: %u\n", sanityChecks[i].state); } } -- 2.50.1 From d36b91a0fd6dfef5bc14366b5cbcf908db0ef6cab6f055387c4694d7f98f8dab Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:57:15 +0100 Subject: [PATCH 15/21] Variable name shadows type name --- main/control.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main/control.c b/main/control.c index 955b624..8b31b0e 100644 --- a/main/control.c +++ b/main/control.c @@ -127,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) @@ -179,7 +179,7 @@ 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) { @@ -195,7 +195,7 @@ void taskControl(void *pvParameters) { ESP_LOGI(TAG, "Enabling burner: Return flow temperature target reached"); - eBurnerState = BURNER_UNKNOWN; + burnerState = BURNER_UNKNOWN; bHeatingInAction = true; setBurnerState(ENABLED); setSafetyControlState(ENABLED); @@ -225,13 +225,13 @@ 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; + burnerState = BURNER_FAULT; setControlState(CONTROL_FAULT_BURNER); setBurnerState(DISABLED); setSafetyControlState(ENABLED); @@ -240,7 +240,7 @@ void taskControl(void *pvParameters) { // ESP_LOGI(TAG, "No burner fault detected: Marking burner as // fired"); - eBurnerState = BURNER_FIRED; + burnerState = BURNER_FIRED; } } } -- 2.50.1 From b7180739077e5c37ac0bb6264dc36453664f30c048a5cad9ee6cea97d1d09fdf Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 11:58:46 +0100 Subject: [PATCH 16/21] Missing break before default --- main/outputs.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main/outputs.c b/main/outputs.c index 6644413..9861b60 100644 --- a/main/outputs.c +++ b/main/outputs.c @@ -97,6 +97,7 @@ void setCirculationPumpState(eOutput in) break; case DISABLED: gpio_set_level(uCirculationPumpGpioPin, 1U); // Switch off Circulation Pump + break; default: break; } @@ -135,6 +136,7 @@ void setBurnerState(eOutput in) break; case DISABLED: gpio_set_level(uBurnerGpioPin, 1U); // Switch off Burner + break; default: break; } @@ -173,6 +175,7 @@ void setSafetyControlState(eOutput in) break; case DISABLED: gpio_set_level(uSafetyContactGpioPin, 1U); // Switch off power for Burner + break; default: break; } -- 2.50.1 From f3f6f1bc5f6bd5d73ca33204fa7127b52be0f1d6ab7b5a32435a211bb79c8b7d Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 12:01:22 +0100 Subject: [PATCH 17/21] Potential division by zero --- main/inputs.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/main/inputs.c b/main/inputs.c index 93b2adf..8df4fb8 100644 --- a/main/inputs.c +++ b/main/inputs.c @@ -132,7 +132,14 @@ void updateAverage(sMeasurement *pMeasurement) 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; @@ -149,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) -- 2.50.1 From f8f6af53bd00e3cea6cb4aa027419b947da5d7ccbb7da25264be9c73ea57eca4 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 12:50:06 +0100 Subject: [PATCH 18/21] implement config --- main/Kconfig.projbuild | 405 +++++++++++++++++++++++++++++++++++++++-- main/control.c | 59 +++--- 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, 492 insertions(+), 101 deletions(-) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index c855f47..5c0bcff 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -1,22 +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" + 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 8b31b0e..8061cc8 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}}}, }; @@ -377,10 +364,10 @@ void findControlCurrentTemperatureEntry(void) /* 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); + gCurrentControlEntry.timestamp.hour, + gCurrentControlEntry.timestamp.minute, + gCurrentControlEntry.fReturnFlowTemperature, + gCurrentControlEntry.fChamberTemperature); */ return; } 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 2b628d9..bf764d4 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 -- 2.50.1 From 1d4e272d800fa1b9309d8576a4d89d4c3db7bc9444a4e04f692270328e4b3a1e Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 13:32:49 +0100 Subject: [PATCH 19/21] error handling and cleanup --- main/control.c | 129 +++++++++++++++++++++--------------------- main/control.h | 107 ++++++++++++++++++++++++++++------- main/inputs.c | 151 +++++++++++++++++++++++++++++++------------------ main/inputs.h | 112 +++++++++++++++++++++++++++++------- main/main.c | 109 +++++++++++++++++++++++++++++++---- main/metrics.c | 78 ++++++++++++++++--------- main/metrics.h | 57 +++++++++++++++---- main/outputs.c | 72 +++++++++++++---------- main/outputs.h | 63 +++++++++++++++++++-- main/safety.c | 75 ++++++++++++++++-------- main/safety.h | 97 +++++++++++++++++++++++++------ main/sntp.c | 26 +++++++-- main/sntp.h | 36 ++++++++++-- main/wifi.c | 43 ++++++++++---- main/wifi.h | 21 ++++++- 15 files changed, 867 insertions(+), 309 deletions(-) diff --git a/main/control.c b/main/control.c index 8061cc8..7a80cf6 100644 --- a/main/control.c +++ b/main/control.c @@ -1,3 +1,8 @@ +/** + * @file control.c + * @brief Implementation of heating control module. + */ + #include "control.h" #include "inputs.h" #include "outputs.h" @@ -9,11 +14,16 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#define PERIODIC_INTERVAL 1U // Run control loop every 1 second +#include + +/** @brief Task interval in seconds. */ +#define PERIODIC_INTERVAL 1U + +static const char *TAG = "control"; -static const char *TAG = "smart-oil-heater-control-system-control"; static eControlState gControlState = CONTROL_STARTING; -// Control table for daily schedules + +/** @brief Weekly schedule table (from Kconfig). */ static const sControlDay gControlTable[] = { {MONDAY, 2U, @@ -72,45 +82,49 @@ static const sControlDay gControlTable[] = { RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMBER_TEMPERATURE_TARGET}}}, }; + 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); +/* Private function prototypes */ +static void taskControl(void *pvParameters); +static void findControlCurrentTemperatureEntry(void); +static void setControlState(eControlState state); -void initControl(void) +esp_err_t initControl(void) { - xMutexAccessControl = xSemaphoreCreateRecursiveMutex(); if (xMutexAccessControl == NULL) { - ESP_LOGE(TAG, "Unable to create mutex"); + ESP_LOGE(TAG, "Failed to create mutex"); + return ESP_FAIL; } xSemaphoreGiveRecursive(xMutexAccessControl); - BaseType_t taskCreated = - xTaskCreate(taskControl, // Function to implement the task - "taskControl", // Task name - 8192, // Stack size (in words, not bytes) - NULL, // Parameters to the task function (none in this case) - 5, // Task priority (higher number = higher priority) - NULL // Task handle (optional) - ); + BaseType_t taskCreated = xTaskCreate( + taskControl, + "taskControl", + 8192, + NULL, + 5, + NULL); - if (taskCreated == pdPASS) - { - ESP_LOGI(TAG, "Task created successfully!"); - } - else + if (taskCreated != pdPASS) { ESP_LOGE(TAG, "Failed to create task"); + return ESP_FAIL; } + + ESP_LOGI(TAG, "Initialized successfully"); + return ESP_OK; } -void taskControl(void *pvParameters) +/** + * @brief Main control task. + * @param pvParameters Task parameters (unused). + */ +static void taskControl(void *pvParameters) { bool bHeatingInAction = false; bool bSummerMode = false; @@ -121,7 +135,7 @@ void taskControl(void *pvParameters) { vTaskDelay(PERIODIC_INTERVAL * 1000U / portTICK_PERIOD_MS); - // Check for safety faults + /* Check for safety faults */ if (getSafetyState() != SAFETY_NO_ERROR) { ESP_LOGW(TAG, "Control not possible due to safety fault!"); @@ -136,7 +150,7 @@ void taskControl(void *pvParameters) continue; } - // Check for SNTP faults + /* Check for SNTP faults */ if (getSntpState() != SYNC_SUCCESSFUL) { ESP_LOGW(TAG, "Control not possible due to SNTP fault!"); @@ -153,35 +167,30 @@ void taskControl(void *pvParameters) findControlCurrentTemperatureEntry(); - if (getOutdoorTemperature().fDampedValue >= - SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH) + /* Summer mode hysteresis */ + if (getOutdoorTemperature().fDampedValue >= SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH) { bSummerMode = true; } - else if (getOutdoorTemperature().fDampedValue <= - SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW) + else if (getOutdoorTemperature().fDampedValue <= SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW) { bSummerMode = false; } - // Enable burner if outdoor temperature is low and return flow temperature - // is cooled down + /* Enable burner if needed */ if (!bHeatingInAction && (burnerState != BURNER_FAULT)) { if (bSummerMode) { - // ESP_LOGI(TAG, "Outdoor temperature too warm: Disabling heating"); setBurnerState(DISABLED); setSafetyControlState(DISABLED); setControlState(CONTROL_OUTDOOR_TOO_WARM); } else if ((getReturnFlowTemperature().average60s.fValue <= getControlCurrentTemperatureEntry().fReturnFlowTemperature) && - (getChamberTemperature().fCurrentValue <= - CHAMBER_TEMPERATURE_THRESHOLD)) + (getChamberTemperature().fCurrentValue <= CHAMBER_TEMPERATURE_THRESHOLD)) { - ESP_LOGI(TAG, - "Enabling burner: Return flow temperature target reached"); + ESP_LOGI(TAG, "Enabling burner: Return flow temperature target reached"); burnerState = BURNER_UNKNOWN; bHeatingInAction = true; setBurnerState(ENABLED); @@ -191,12 +200,11 @@ void taskControl(void *pvParameters) } else { - // ESP_LOGI(TAG, "Return flow temperature too warm: Disabling heating"); setControlState(CONTROL_RETURN_FLOW_TOO_WARM); } } - // Disable burner if target temperature is reached or a fault occurred + /* Disable burner if target reached or fault */ if (bHeatingInAction) { if ((getChamberTemperature().fCurrentValue >= @@ -233,9 +241,8 @@ void taskControl(void *pvParameters) } } - // Manage circulation pump - if (getChamberTemperature().fCurrentValue <= - CIRCULATION_PUMP_TEMPERATURE_THRESHOLD) + /* Manage circulation pump */ + if (getChamberTemperature().fCurrentValue <= CIRCULATION_PUMP_TEMPERATURE_THRESHOLD) { // ESP_LOGI(TAG, "Burner cooled down: Disabling circulation pump"); setCirculationPumpState(DISABLED); @@ -248,9 +255,12 @@ void taskControl(void *pvParameters) } // End of while(1) } -void setControlState(eControlState state) +/** + * @brief Set the control state with mutex protection. + * @param state New control state. + */ +static void setControlState(eControlState state) { - if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE) { gControlState = state; @@ -264,7 +274,6 @@ void setControlState(eControlState state) eControlState getControlState(void) { - eControlState ret = CONTROL_FAULT_SAFETY; if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE) @@ -282,7 +291,6 @@ eControlState getControlState(void) eControlWeekday getControlCurrentWeekday(void) { - // Get current time time_t now; struct tm timeinfo; time(&now); @@ -293,24 +301,12 @@ eControlWeekday getControlCurrentWeekday(void) } /** - * @brief Finds the active temperature control entry for the current time. - * - * Searches through the weekly schedule to find the most recent entry - * that should be active at the current date/time. Falls back to the - * last entry in the week if no suitable entry is found. + * @brief Find the currently active temperature entry based on time. */ -/** - * @brief Finds the active temperature control entry for the current time. - * - * Searches through the weekly schedule to find the most recent entry - * that should be active at the current date/time. Falls back to the - * last entry in the week if no suitable entry is found. - */ -void findControlCurrentTemperatureEntry(void) +static void findControlCurrentTemperatureEntry(void) { eControlWeekday currentDay = getControlCurrentWeekday(); - // Get current time time_t now; struct tm timeinfo; time(&now); @@ -361,14 +357,15 @@ void findControlCurrentTemperatureEntry(void) const sControlDay *sunday = &gControlTable[6]; gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1]; } + xSemaphoreGiveRecursive(xMutexAccessControl); /* 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); - */ + "Return Temp: %lf, Chamber Temp: %lf", + gCurrentControlEntry.timestamp.hour, + gCurrentControlEntry.timestamp.minute, + gCurrentControlEntry.fReturnFlowTemperature, + gCurrentControlEntry.fChamberTemperature); + */ return; } } diff --git a/main/control.h b/main/control.h index 0096038..4507f95 100644 --- a/main/control.h +++ b/main/control.h @@ -1,38 +1,74 @@ +/** + * @file control.h + * @brief Heating control logic with schedule-based temperature management. + * + * This module implements the main heating control loop. It manages + * burner operation based on time schedules, temperature targets, + * and summer mode detection. + */ + #pragma once #include "sdkconfig.h" +#include "esp_err.h" -#include +#include +#include +/** @brief Maximum number of temperature entries per day. */ #define MAX_TEMPERATURE_ENTRIES_PER_DAY 24U +/** @brief Return flow target temperature for day mode (°C). */ #define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY (CONFIG_TEMP_RETURN_FLOW_LOWER_LIMIT_DAY / 10.0f) + +/** @brief Return flow target temperature for night mode (°C). */ #define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT (CONFIG_TEMP_RETURN_FLOW_LOWER_LIMIT_NIGHT / 10.0f) + +/** @brief Chamber target temperature (°C). */ #define CHAMBER_TEMPERATURE_TARGET (CONFIG_TEMP_CHAMBER_TARGET / 10.0f) + +/** @brief Chamber temperature threshold to enable burner (°C). */ #define CHAMBER_TEMPERATURE_THRESHOLD (CONFIG_TEMP_CHAMBER_THRESHOLD / 10.0f) + +/** @brief Outdoor temperature to activate summer mode (°C). */ #define SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH (CONFIG_TEMP_SUMMER_MODE_HIGH / 10.0f) + +/** @brief Outdoor temperature to deactivate summer mode (°C). */ #define SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW (CONFIG_TEMP_SUMMER_MODE_LOW / 10.0f) + +/** @brief Chamber temperature threshold for circulation pump (°C). */ #define CIRCULATION_PUMP_TEMPERATURE_THRESHOLD (CONFIG_TEMP_CIRCULATION_PUMP_THRESHOLD / 10.0f) + +/** @brief Time to wait before checking burner fault (seconds). */ #define BURNER_FAULT_DETECTION_THRESHOLD CONFIG_BURNER_FAULT_DETECTION_SECONDS +/** + * @brief Control state enumeration. + */ typedef enum _ControlState { - CONTROL_STARTING, - CONTROL_HEATING, - CONTROL_OUTDOOR_TOO_WARM, - CONTROL_RETURN_FLOW_TOO_WARM, - CONTROL_FAULT_BURNER, - CONTROL_FAULT_SAFETY, - CONTROL_FAULT_SNTP, + CONTROL_STARTING, /**< System starting up. */ + CONTROL_HEATING, /**< Burner running. */ + CONTROL_OUTDOOR_TOO_WARM, /**< Summer mode active. */ + CONTROL_RETURN_FLOW_TOO_WARM, /**< Target temperature reached. */ + CONTROL_FAULT_BURNER, /**< Burner fault detected. */ + CONTROL_FAULT_SAFETY, /**< Safety fault detected. */ + CONTROL_FAULT_SNTP, /**< SNTP sync failed. */ } eControlState; +/** + * @brief Burner operational state enumeration. + */ typedef enum _BurnerState { - BURNER_UNKNOWN, // Burner is disabled or state after enabling is still unkown - BURNER_FIRED, // Burner fired successfully - BURNER_FAULT // Burner was unable to fire successfully + BURNER_UNKNOWN, /**< Burner state unknown after enable. */ + BURNER_FIRED, /**< Burner fired successfully. */ + BURNER_FAULT /**< Burner failed to fire. */ } eBurnerState; +/** + * @brief Weekday enumeration (Monday = 0). + */ typedef enum _ControlWeekday { MONDAY, @@ -44,27 +80,58 @@ typedef enum _ControlWeekday SUNDAY, } eControlWeekday; +/** + * @brief Time of day structure. + */ typedef struct _ControlTimestamp { - uint8_t hour; - uint8_t minute; + uint8_t hour; /**< Hour (0-23). */ + uint8_t minute; /**< Minute (0-59). */ } sControlTimestamp; +/** + * @brief Temperature schedule entry. + */ typedef struct _ControlTemperatureEntry { - sControlTimestamp timestamp; - float fReturnFlowTemperature; - float fChamberTemperature; + sControlTimestamp timestamp; /**< Time when entry becomes active. */ + float fReturnFlowTemperature; /**< Target return flow temperature. */ + float fChamberTemperature; /**< Target chamber temperature. */ } sControlTemperatureEntry; +/** + * @brief Daily schedule structure. + */ typedef struct _ControlDay { - eControlWeekday day; - size_t entryCount; // number of entries for each day - sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY]; + eControlWeekday day; /**< Day of week. */ + size_t entryCount; /**< Number of entries for this day. */ + sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY]; /**< Schedule entries. */ } sControlDay; -void initControl(void); +/** + * @brief Initialize the control module. + * + * Creates the control task that manages heating operation. + * + * @return ESP_OK on success, ESP_FAIL on error. + */ +esp_err_t initControl(void); + +/** + * @brief Get the current control state. + * @return eControlState indicating current operation mode. + */ eControlState getControlState(void); + +/** + * @brief Get the current weekday. + * @return eControlWeekday (Monday = 0, Sunday = 6). + */ eControlWeekday getControlCurrentWeekday(void); + +/** + * @brief Get the currently active temperature entry. + * @return sControlTemperatureEntry with current targets. + */ sControlTemperatureEntry getControlCurrentTemperatureEntry(void); diff --git a/main/inputs.c b/main/inputs.c index 56c04b2..9289e9c 100644 --- a/main/inputs.c +++ b/main/inputs.c @@ -1,3 +1,8 @@ +/** + * @file inputs.c + * @brief Implementation of input handling module. + */ + #include "inputs.h" #include "freertos/FreeRTOS.h" @@ -9,22 +14,38 @@ #include #include +/** @brief Maximum number of DS18B20 sensors supported. */ #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 = CONFIG_GPIO_BURNER_FAULT; -const uint8_t uDS18B20Pin = CONFIG_GPIO_DS18B20_ONEWIRE; +/** @brief Number of retry attempts for 1-Wire read. */ +#define ONE_WIRE_LOOPS 4U -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; +/** @brief Task interval in seconds. */ +#define PERIODIC_INTERVAL 1U -onewire_addr_t uOneWireAddresses[MAX_DN18B20_SENSORS]; -float fDS18B20Temps[MAX_DN18B20_SENSORS]; -size_t sSensorCount = 0U; +static const char *TAG = "inputs"; + +/** @brief Burner fault GPIO pin (from Kconfig). */ +static const uint8_t uBurnerFaultPin = CONFIG_GPIO_BURNER_FAULT; + +/** @brief DS18B20 1-Wire GPIO pin (from Kconfig). */ +static const uint8_t uDS18B20Pin = CONFIG_GPIO_DS18B20_ONEWIRE; + +/** @brief Chamber temperature sensor address (from Kconfig). */ +static const onewire_addr_t uChamperTempSensorAddr = CONFIG_ONEWIRE_ADDR_CHAMBER_TEMP; + +/** @brief Outdoor temperature sensor address (from Kconfig). */ +static const onewire_addr_t uOutdoorTempSensorAddr = CONFIG_ONEWIRE_ADDR_OUTDOOR_TEMP; + +/** @brief Inlet flow temperature sensor address (from Kconfig). */ +static const onewire_addr_t uInletFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_INLET_FLOW_TEMP; + +/** @brief Return flow temperature sensor address (from Kconfig). */ +static const onewire_addr_t uReturnFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_RETURN_FLOW_TEMP; + +static onewire_addr_t uOneWireAddresses[MAX_DN18B20_SENSORS]; +static float fDS18B20Temps[MAX_DN18B20_SENSORS]; +static size_t sSensorCount = 0U; static SemaphoreHandle_t xMutexAccessInputs = NULL; static eBurnerErrorState sBurnerErrorState; @@ -33,34 +54,34 @@ static sMeasurement sOutdoorTemperature; static sMeasurement sInletFlowTemperature; static sMeasurement sReturnFlowTemperature; -void taskInput(void *pvParameters); -void initMeasurement(sMeasurement *pMeasurement); -void updateAverage(sMeasurement *pMeasurement); -void updatePrediction(sMeasurement *pMeasurement); -float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex); +/* Private function prototypes */ +static void taskInput(void *pvParameters); +static void initMeasurement(sMeasurement *pMeasurement); +static void updateAverage(sMeasurement *pMeasurement); +static void updatePrediction(sMeasurement *pMeasurement); +static float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex); -void initInputs(void) +esp_err_t initInputs(void) { - gpio_config_t ioConfBurnerFault = { - .pin_bit_mask = (1ULL << uBurnerFaultPin), // Pin mask - .mode = GPIO_MODE_INPUT, // Set as inout - .pull_up_en = GPIO_PULLUP_ENABLE, // Enable pull-up - .pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down - .intr_type = GPIO_INTR_DISABLE // Disable interrupts - }; + .pin_bit_mask = (1ULL << uBurnerFaultPin), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE}; esp_err_t ret = gpio_config(&ioConfBurnerFault); if (ret != ESP_OK) { ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); - return; + return ESP_FAIL; } xMutexAccessInputs = xSemaphoreCreateRecursiveMutex(); if (xMutexAccessInputs == NULL) { - ESP_LOGE(TAG, "Unable to create mutex"); + ESP_LOGE(TAG, "Failed to create mutex"); + return ESP_FAIL; } xSemaphoreGiveRecursive(xMutexAccessInputs); @@ -70,25 +91,28 @@ void initInputs(void) initMeasurement(&sReturnFlowTemperature); BaseType_t taskCreated = xTaskCreate( - taskInput, // Function to implement the task - "taskInput", // Task name - 4096, // Stack size (in words, not bytes) - NULL, // Parameters to the task function (none in this case) - 5, // Task priority (higher number = higher priority) - NULL // Task handle (optional) - ); + taskInput, + "taskInput", + 4096, + NULL, + 5, + NULL); - if (taskCreated == pdPASS) - { - ESP_LOGI(TAG, "Task created successfully!"); - } - else + if (taskCreated != pdPASS) { ESP_LOGE(TAG, "Failed to create task"); + return ESP_FAIL; } + + ESP_LOGI(TAG, "Initialized successfully"); + return ESP_OK; } -void initMeasurement(sMeasurement *pMeasurement) +/** + * @brief Initialize a measurement structure to default values. + * @param pMeasurement Pointer to measurement structure. + */ +static void initMeasurement(sMeasurement *pMeasurement) { if (!pMeasurement) return; @@ -113,12 +137,16 @@ void initMeasurement(sMeasurement *pMeasurement) memset(pMeasurement->predict60s.samples, 0U, sizeof(float) * PRED60S_SAMPLE_SIZE); } -void updateAverage(sMeasurement *pMeasurement) +/** + * @brief Update average values and damped value for a measurement. + * @param pMeasurement Pointer to measurement structure. + */ +static void updateAverage(sMeasurement *pMeasurement) { if (!pMeasurement) return; - // Average form the last 10sec + /* 10-second average */ pMeasurement->average10s.samples[pMeasurement->average10s.bufferIndex] = pMeasurement->fCurrentValue; pMeasurement->average10s.bufferIndex = (pMeasurement->average10s.bufferIndex + 1) % AVG10S_SAMPLE_SIZE; @@ -142,7 +170,7 @@ void updateAverage(sMeasurement *pMeasurement) pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount; } - // Average form the last 60sec + /* 60-second average */ pMeasurement->average60s.samples[pMeasurement->average60s.bufferIndex] = pMeasurement->fCurrentValue; pMeasurement->average60s.bufferIndex = (pMeasurement->average60s.bufferIndex + 1) % AVG60S_SAMPLE_SIZE; @@ -166,7 +194,7 @@ void updateAverage(sMeasurement *pMeasurement) pMeasurement->average60s.fValue = sum / pMeasurement->average60s.bufferCount; } - // Damped current value + /* Damped current value */ if (pMeasurement->fDampedValue == INITIALISATION_VALUE) { pMeasurement->fDampedValue = pMeasurement->fCurrentValue; @@ -185,7 +213,11 @@ void updateAverage(sMeasurement *pMeasurement) } } -void updatePrediction(sMeasurement *pMeasurement) +/** + * @brief Update 60-second prediction using linear regression. + * @param pMeasurement Pointer to measurement structure. + */ +static void updatePrediction(sMeasurement *pMeasurement) { if (!pMeasurement) return; @@ -205,7 +237,11 @@ void updatePrediction(sMeasurement *pMeasurement) predict60s->bufferCount + 60.0f); } -void taskInput(void *pvParameters) +/** + * @brief Input task - reads sensors periodically. + * @param pvParameters Task parameters (unused). + */ +static void taskInput(void *pvParameters) { while (1) { @@ -307,20 +343,27 @@ void taskInput(void *pvParameters) } } -float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex) +/** + * @brief Predict future value using linear regression. + * @param samples Sample buffer. + * @param count Number of valid samples. + * @param bufferIndex Current buffer write index. + * @param futureIndex Future time index to predict. + * @return Predicted value. + */ +static float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex) { if (count == 0) - return INITIALISATION_VALUE; // No prediction possible with no data + return INITIALISATION_VALUE; float sumX = INITIALISATION_VALUE, sumY = INITIALISATION_VALUE, sumXY = INITIALISATION_VALUE, sumX2 = INITIALISATION_VALUE; for (size_t i = 0; i < count; i++) { - // Calculate the circular buffer index for the current sample size_t circularIndex = (bufferIndex + i + 1) % count; - float x = (float)i; // Time index - float y = samples[circularIndex]; // Sample value + float x = (float)i; + float y = samples[circularIndex]; sumX += x; sumY += y; @@ -328,15 +371,13 @@ float linearRegressionPredict(const float *samples, size_t count, size_t bufferI sumX2 += x * x; } - // Calculate slope (m) and intercept (b) of the line: y = mx + b float denominator = (count * sumX2 - sumX * sumX); - if (fabs(denominator) < 1e-6) // Avoid division by zero - return samples[bufferIndex]; // Return the latest value as prediction + if (fabs(denominator) < 1e-6) + return samples[bufferIndex]; float m = (count * sumXY - sumX * sumY) / denominator; float b = (sumY - m * sumX) / count; - // Predict value at futureIndex return m * futureIndex + b; } diff --git a/main/inputs.h b/main/inputs.h index be68658..7d8fb59 100644 --- a/main/inputs.h +++ b/main/inputs.h @@ -1,59 +1,133 @@ +/** + * @file inputs.h + * @brief Input handling for temperature sensors and burner fault detection. + * + * This module reads DS18B20 temperature sensors via 1-Wire and monitors + * the burner fault input. It provides averaged, damped, and predicted + * temperature values. + */ + #pragma once #include "sdkconfig.h" +#include "esp_err.h" + #include +/** @brief Returns the maximum of two values. */ #define MAX(a, b) ((a) > (b) ? (a) : (b)) + +/** @brief Initial value for measurements before first reading. */ #define INITIALISATION_VALUE 0.0f + +/** @brief Sample buffer size for 10-second average. */ #define AVG10S_SAMPLE_SIZE 10U + +/** @brief Sample buffer size for 60-second average. */ #define AVG60S_SAMPLE_SIZE 60U + +/** @brief Sample buffer size for 24-hour average. */ #define AVG24H_SAMPLE_SIZE 24U + +/** @brief Sample buffer size for 60-second prediction. */ #define PRED60S_SAMPLE_SIZE 60U + +/** @brief Damping factor for rising temperatures (from Kconfig). */ #define DAMPING_FACTOR_WARMER (CONFIG_DAMPING_FACTOR_WARMER * 0.00001f) + +/** @brief Damping factor for falling temperatures (from Kconfig). */ #define DAMPING_FACTOR_COLDER (CONFIG_DAMPING_FACTOR_COLDER * 0.00001f) +/** + * @brief Burner error state enumeration. + */ typedef enum _BurnerErrorState { - NO_ERROR, - FAULT + NO_ERROR, /**< No burner fault detected. */ + FAULT /**< Burner fault signal active. */ } eBurnerErrorState; +/** + * @brief Measurement error state enumeration. + */ typedef enum _MeasurementErrorState { - MEASUREMENT_NO_ERROR, - MEASUREMENT_FAULT + MEASUREMENT_NO_ERROR, /**< Measurement valid. */ + MEASUREMENT_FAULT /**< Measurement failed or sensor not found. */ } eMeasurementErrorState; +/** + * @brief Circular buffer for averaging temperature values. + */ typedef struct _Average { - float fValue; - float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))]; - size_t bufferIndex; - size_t bufferCount; + float fValue; /**< Current average value. */ + float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))]; /**< Sample buffer. */ + size_t bufferIndex; /**< Current write index. */ + size_t bufferCount; /**< Number of valid samples. */ } sAverage; +/** + * @brief Circular buffer for temperature prediction. + */ typedef struct _Predict { - float fValue; - float samples[PRED60S_SAMPLE_SIZE]; - size_t bufferIndex; - size_t bufferCount; + float fValue; /**< Predicted value. */ + float samples[PRED60S_SAMPLE_SIZE]; /**< Sample buffer. */ + size_t bufferIndex; /**< Current write index. */ + size_t bufferCount; /**< Number of valid samples. */ } sPredict; +/** + * @brief Complete measurement data structure. + */ typedef struct _Measurement { - float fCurrentValue; - float fDampedValue; - sAverage average10s; - sAverage average60s; - sPredict predict60s; - eMeasurementErrorState state; + float fCurrentValue; /**< Current raw temperature value. */ + float fDampedValue; /**< Damped temperature value. */ + sAverage average10s; /**< 10-second rolling average. */ + sAverage average60s; /**< 60-second rolling average. */ + sPredict predict60s; /**< 60-second prediction. */ + eMeasurementErrorState state; /**< Measurement state. */ } sMeasurement; -void initInputs(void); +/** + * @brief Initialize the inputs module. + * + * Configures GPIO for burner fault input and starts the input task + * for reading DS18B20 temperature sensors. + * + * @return ESP_OK on success, ESP_FAIL on error. + */ +esp_err_t initInputs(void); + +/** + * @brief Get the current chamber temperature measurement. + * @return sMeasurement structure with current values and state. + */ sMeasurement getChamberTemperature(void); + +/** + * @brief Get the current outdoor temperature measurement. + * @return sMeasurement structure with current values and state. + */ sMeasurement getOutdoorTemperature(void); + +/** + * @brief Get the current inlet flow temperature measurement. + * @return sMeasurement structure with current values and state. + */ sMeasurement getInletFlowTemperature(void); + +/** + * @brief Get the current return flow temperature measurement. + * @return sMeasurement structure with current values and state. + */ sMeasurement getReturnFlowTemperature(void); + +/** + * @brief Get the current burner error state. + * @return eBurnerErrorState indicating fault status. + */ eBurnerErrorState getBurnerError(void); diff --git a/main/main.c b/main/main.c index 622cd51..ba373de 100644 --- a/main/main.c +++ b/main/main.c @@ -1,3 +1,11 @@ +/** + * @file main.c + * @brief Main entry point for Smart Oil Heating Control System. + * + * This file initializes all system modules and handles initialization + * errors by logging diagnostic information and triggering a reboot. + */ + #include "safety.h" #include "metrics.h" #include "outputs.h" @@ -9,30 +17,107 @@ #include "esp_log.h" #include "esp_system.h" #include "nvs_flash.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" -static const char *TAG = "smart-oil-heater-control-system"; +static const char *TAG = "main"; +/** @brief Delay before reboot in milliseconds. */ +#define REBOOT_DELAY_MS 5000 + +/** + * @brief Log error and trigger system reboot. + * @param module Name of the module that failed. + */ +static void reboot_on_error(const char *module) +{ + ESP_LOGE(TAG, "========================================"); + ESP_LOGE(TAG, "FATAL: %s initialization failed!", module); + ESP_LOGE(TAG, "System will reboot in %d seconds...", REBOOT_DELAY_MS / 1000); + ESP_LOGE(TAG, "========================================"); + + vTaskDelay(pdMS_TO_TICKS(REBOOT_DELAY_MS)); + esp_restart(); +} + +/** + * @brief Application main entry point. + */ void app_main(void) { - ESP_LOGI(TAG, "starting ..."); + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, "Smart Oil Heating Control System"); + ESP_LOGI(TAG, "Starting initialization..."); + ESP_LOGI(TAG, "========================================"); - // Initialize NVS + /* 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_LOGW(TAG, "NVS partition needs erase, erasing..."); ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } - ESP_ERROR_CHECK(ret); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "NVS init failed: %s", esp_err_to_name(ret)); + reboot_on_error("NVS"); + } + ESP_LOGI(TAG, "[OK] NVS initialized"); - // TODO: Error handling! - initOutputs(); - initInputs(); - initSafety(); - initWifi(); - initSntp(); - initControl(); - initMetrics(); + /* Initialize Outputs */ + if (initOutputs() != ESP_OK) + { + reboot_on_error("Outputs"); + } + ESP_LOGI(TAG, "[OK] Outputs initialized"); + + /* Initialize Inputs */ + if (initInputs() != ESP_OK) + { + reboot_on_error("Inputs"); + } + ESP_LOGI(TAG, "[OK] Inputs initialized"); + + /* Initialize Safety */ + if (initSafety() != ESP_OK) + { + reboot_on_error("Safety"); + } + ESP_LOGI(TAG, "[OK] Safety initialized"); + + /* Initialize WiFi */ + if (initWifi() != ESP_OK) + { + reboot_on_error("WiFi"); + } + ESP_LOGI(TAG, "[OK] WiFi initialized"); + + /* Initialize SNTP */ + if (initSntp() != ESP_OK) + { + reboot_on_error("SNTP"); + } + ESP_LOGI(TAG, "[OK] SNTP initialized"); + + /* Initialize Control */ + if (initControl() != ESP_OK) + { + reboot_on_error("Control"); + } + ESP_LOGI(TAG, "[OK] Control initialized"); + + /* Initialize Metrics */ + if (initMetrics() != ESP_OK) + { + reboot_on_error("Metrics"); + } + ESP_LOGI(TAG, "[OK] Metrics initialized"); + + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, "All modules initialized successfully!"); + ESP_LOGI(TAG, "System is now running."); + ESP_LOGI(TAG, "========================================"); while (1) { diff --git a/main/metrics.c b/main/metrics.c index ba3c436..d24d45f 100644 --- a/main/metrics.c +++ b/main/metrics.c @@ -1,3 +1,8 @@ +/** + * @file metrics.c + * @brief Implementation of Prometheus metrics endpoint. + */ + #include "metrics.h" #include "outputs.h" #include "inputs.h" @@ -15,41 +20,50 @@ #include #include -static const char *TAG = "smart-oil-heater-control-system-metrics"; +static const char *TAG = "metrics"; -char caHtmlResponse[HTML_RESPONSE_SIZE]; -SemaphoreHandle_t xMutexAccessMetricResponse = NULL; +static char caHtmlResponse[HTML_RESPONSE_SIZE]; +static SemaphoreHandle_t xMutexAccessMetricResponse = NULL; static sMetric aMetrics[METRIC_MAX_COUNT]; static uint16_t u16MetricCounter = 0U; -void taskMetrics(void *pvParameters); -httpd_handle_t setup_server(void); -esp_err_t get_metrics_handler(httpd_req_t *req); +/* Private function prototypes */ +static void taskMetrics(void *pvParameters); +static httpd_handle_t setup_server(void); +static esp_err_t get_metrics_handler(httpd_req_t *req); -void initMetrics(void) +esp_err_t initMetrics(void) { - setup_server(); + httpd_handle_t server = setup_server(); + if (server == NULL) + { + ESP_LOGE(TAG, "Failed to start HTTP server"); + return ESP_FAIL; + } BaseType_t taskCreated = xTaskCreate( - taskMetrics, // Function to implement the task - "taskMetrics", // Task name - 32768, // Stack size (in words, not bytes) - NULL, // Parameters to the task function (none in this case) - 5, // Task priority (higher number = higher priority) - NULL // Task handle (optional) - ); + taskMetrics, + "taskMetrics", + 32768, + NULL, + 5, + NULL); - if (taskCreated == pdPASS) - { - ESP_LOGI(TAG, "Task created successfully!"); - } - else + if (taskCreated != pdPASS) { ESP_LOGE(TAG, "Failed to create task"); + return ESP_FAIL; } + + ESP_LOGI(TAG, "Initialized successfully"); + return ESP_OK; } -void taskMetrics(void *pvParameters) +/** + * @brief Metrics collection task. + * @param pvParameters Task parameters (unused). + */ +static void taskMetrics(void *pvParameters) { while (1) { @@ -338,8 +352,6 @@ void vSetMetrics(sMetric *paMetrics, uint16_t u16Size) break; } - // printf("%s\n", paMetrics[u16Index].caMetricName); - // printf("%s\n", caValueBuffer); strcat(caHtmlResponse, paMetrics[u16Index].caMetricName); strcat(caHtmlResponse, caValueBuffer); strcat(caHtmlResponse, "\n"); @@ -352,7 +364,12 @@ void vSetMetrics(sMetric *paMetrics, uint16_t u16Size) } } -esp_err_t get_metrics_handler(httpd_req_t *req) +/** + * @brief HTTP GET handler for /metrics endpoint. + * @param req HTTP request. + * @return ESP_OK on success. + */ +static esp_err_t get_metrics_handler(httpd_req_t *req) { if (xSemaphoreTakeRecursive(xMutexAccessMetricResponse, pdMS_TO_TICKS(5000)) == pdTRUE) { @@ -367,7 +384,11 @@ esp_err_t get_metrics_handler(httpd_req_t *req) } } -httpd_handle_t setup_server(void) +/** + * @brief Setup HTTP server for metrics endpoint. + * @return HTTP server handle or NULL on failure. + */ +static httpd_handle_t setup_server(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = 9100; @@ -382,14 +403,17 @@ httpd_handle_t setup_server(void) xMutexAccessMetricResponse = xSemaphoreCreateRecursiveMutex(); if (xMutexAccessMetricResponse == NULL) { - ESP_LOGE(TAG, "Unable to create mutex for metric response"); + ESP_LOGE(TAG, "Failed to create mutex"); + return NULL; } xSemaphoreGiveRecursive(xMutexAccessMetricResponse); if (httpd_start(&server, &config) == ESP_OK) { httpd_register_uri_handler(server, &uri_get); + return server; } - return server; + ESP_LOGE(TAG, "Failed to start HTTP server"); + return NULL; } diff --git a/main/metrics.h b/main/metrics.h index e8eafed..2515b4e 100644 --- a/main/metrics.h +++ b/main/metrics.h @@ -1,26 +1,61 @@ +/** + * @file metrics.h + * @brief Prometheus metrics HTTP endpoint. + * + * This module provides a HTTP server on port 9100 that exposes + * system metrics in Prometheus format at /metrics endpoint. + */ + #pragma once -#include +#include "esp_err.h" +#include "esp_http_server.h" +#include + +/** @brief Maximum size of HTTP response buffer. */ #define HTML_RESPONSE_SIZE 4096U + +/** @brief Maximum length of metric name. */ #define METRIC_NAME_MAX_SIZE 64U + +/** @brief Maximum number of metrics. */ #define METRIC_MAX_COUNT 38U +/** + * @brief Metric value type enumeration. + */ typedef enum _MetricValueType { - FLOAT, - INTEGER_U8, - INTEGER_64, + FLOAT, /**< Floating point value. */ + INTEGER_U8, /**< 8-bit unsigned integer. */ + INTEGER_64, /**< 64-bit signed integer. */ } eMetricValueType; +/** + * @brief Metric data structure. + */ typedef struct _metric { - char caMetricName[METRIC_NAME_MAX_SIZE]; - eMetricValueType type; - float fMetricValue; - uint8_t u8MetricValue; - int64_t i64MetricValue; + char caMetricName[METRIC_NAME_MAX_SIZE]; /**< Metric name. */ + eMetricValueType type; /**< Value type. */ + float fMetricValue; /**< Float value (if type is FLOAT). */ + uint8_t u8MetricValue; /**< U8 value (if type is INTEGER_U8). */ + int64_t i64MetricValue; /**< I64 value (if type is INTEGER_64). */ } sMetric; -void initMetrics(void); -void vSetMetrics(sMetric *paMetrics, uint16_t u16Size); \ No newline at end of file +/** + * @brief Initialize the metrics module. + * + * Starts the HTTP server and creates the metrics collection task. + * + * @return ESP_OK on success, ESP_FAIL on error. + */ +esp_err_t initMetrics(void); + +/** + * @brief Update the metrics buffer. + * @param paMetrics Array of metrics to publish. + * @param u16Size Number of metrics in array. + */ +void vSetMetrics(sMetric *paMetrics, uint16_t u16Size); diff --git a/main/outputs.c b/main/outputs.c index 63dd0e6..9475d89 100644 --- a/main/outputs.c +++ b/main/outputs.c @@ -1,3 +1,8 @@ +/** + * @file outputs.c + * @brief Implementation of output control module. + */ + #include "outputs.h" #include "sdkconfig.h" @@ -6,69 +11,76 @@ #include "driver/gpio.h" #include "esp_log.h" -static const char *TAG = "smart-oil-heater-control-system-outputs"; -const uint8_t uCirculationPumpGpioPin = CONFIG_GPIO_CIRCULATION_PUMP; -const uint8_t uBurnerGpioPin = CONFIG_GPIO_BURNER; -const uint8_t uSafetyContactGpioPin = CONFIG_GPIO_SAFETY_CONTACT; +static const char *TAG = "outputs"; + +/** @brief Circulation pump GPIO pin (from Kconfig). */ +static const uint8_t uCirculationPumpGpioPin = CONFIG_GPIO_CIRCULATION_PUMP; + +/** @brief Burner control GPIO pin (from Kconfig). */ +static const uint8_t uBurnerGpioPin = CONFIG_GPIO_BURNER; + +/** @brief Safety contact GPIO pin (from Kconfig). */ +static const uint8_t uSafetyContactGpioPin = CONFIG_GPIO_SAFETY_CONTACT; static SemaphoreHandle_t xMutexAccessOutputs = NULL; static eOutput sCirculationPumpState; static eOutput sBurnerState; static eOutput sSafetyContactState; -void initOutputs(void) +esp_err_t initOutputs(void) { gpio_config_t ioConfCirculationPump = { - .pin_bit_mask = (1ULL << uCirculationPumpGpioPin), // Pin mask - .mode = GPIO_MODE_OUTPUT, // Set as output - .pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up - .pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down - .intr_type = GPIO_INTR_DISABLE // Disable interrupts - }; + .pin_bit_mask = (1ULL << uCirculationPumpGpioPin), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE}; gpio_config_t ioConfBurner = { - .pin_bit_mask = (1ULL << uBurnerGpioPin), // Pin mask - .mode = GPIO_MODE_OUTPUT, // Set as output - .pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up - .pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down - .intr_type = GPIO_INTR_DISABLE // Disable interrupts - }; + .pin_bit_mask = (1ULL << uBurnerGpioPin), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE}; gpio_config_t ioConfSafetyContact = { - .pin_bit_mask = (1ULL << uSafetyContactGpioPin), // Pin mask - .mode = GPIO_MODE_OUTPUT, // Set as output - .pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up - .pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down - .intr_type = GPIO_INTR_DISABLE // Disable interrupts - }; + .pin_bit_mask = (1ULL << uSafetyContactGpioPin), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE}; esp_err_t ret = gpio_config(&ioConfCirculationPump); if (ret != ESP_OK) { - ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); - return; + ESP_LOGE(TAG, "GPIO config failed for circulation pump: %s", esp_err_to_name(ret)); + return ESP_FAIL; } ret = gpio_config(&ioConfBurner); if (ret != ESP_OK) { - ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); - return; + ESP_LOGE(TAG, "GPIO config failed for burner: %s", esp_err_to_name(ret)); + return ESP_FAIL; } ret = gpio_config(&ioConfSafetyContact); if (ret != ESP_OK) { - ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); - return; + ESP_LOGE(TAG, "GPIO config failed for safety contact: %s", esp_err_to_name(ret)); + return ESP_FAIL; } xMutexAccessOutputs = xSemaphoreCreateRecursiveMutex(); if (xMutexAccessOutputs == NULL) { - ESP_LOGE(TAG, "Unable to create mutex"); + ESP_LOGE(TAG, "Failed to create mutex"); + return ESP_FAIL; } xSemaphoreGiveRecursive(xMutexAccessOutputs); + + ESP_LOGI(TAG, "Initialized successfully"); + return ESP_OK; } eOutput getCirculationPumpState(void) diff --git a/main/outputs.h b/main/outputs.h index 12ddfe9..c064178 100644 --- a/main/outputs.h +++ b/main/outputs.h @@ -1,15 +1,70 @@ +/** + * @file outputs.h + * @brief Output control for circulation pump, burner, and safety contact. + * + * This module controls the relay outputs via GPIO pins. All outputs + * are active-low (relay energized when GPIO is low). + */ + #pragma once +#include "esp_err.h" + +/** + * @brief Output state enumeration. + */ typedef enum _Output { - ENABLED, - DISABLED + ENABLED, /**< Output active (relay energized, GPIO low). */ + DISABLED /**< Output inactive (relay de-energized, GPIO high). */ } eOutput; -void initOutputs(void); +/** + * @brief Initialize the outputs module. + * + * Configures GPIO pins for circulation pump, burner, and safety contact + * as outputs. All outputs are initialized to DISABLED (safe state). + * + * @return ESP_OK on success, ESP_FAIL on error. + */ +esp_err_t initOutputs(void); + +/** + * @brief Get the current circulation pump state. + * @return eOutput state (ENABLED or DISABLED). + */ eOutput getCirculationPumpState(void); + +/** + * @brief Set the circulation pump state. + * @param in Desired state (ENABLED or DISABLED). + */ void setCirculationPumpState(eOutput in); + +/** + * @brief Get the current burner state. + * @return eOutput state (ENABLED or DISABLED). + */ eOutput getBurnerState(void); + +/** + * @brief Set the burner state. + * @param in Desired state (ENABLED or DISABLED). + */ void setBurnerState(eOutput in); + +/** + * @brief Get the current safety contact state. + * @return eOutput state (ENABLED or DISABLED). + */ eOutput getSafetyControlState(void); -void setSafetyControlState(eOutput in); \ No newline at end of file + +/** + * @brief Set the safety contact state. + * + * The safety contact controls power to the burner. When DISABLED, + * the burner cannot operate regardless of the burner signal. + * + * @param in Desired state (ENABLED or DISABLED). + */ +void setSafetyControlState(eOutput in); diff --git a/main/safety.c b/main/safety.c index 7fb6ae4..fbf3d27 100644 --- a/main/safety.c +++ b/main/safety.c @@ -1,3 +1,8 @@ +/** + * @file safety.c + * @brief Implementation of safety monitoring module. + */ + #include "safety.h" #include "freertos/FreeRTOS.h" @@ -7,53 +12,68 @@ #include #include -#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 +/** @brief Task interval in seconds. */ +#define PERIODIC_INTERVAL 1U + +/** @brief Grace period for unchanged sensor readings (seconds). */ +#define SENSOR_GRACE_PERIOD (CONFIG_SENSOR_GRACE_PERIOD_MINUTES * 60U) + +/** @brief Epsilon for float comparison. */ #define FLOAT_EPSILON 0.0001f -static const char *TAG = "smart-oil-heater-control-system-safety"; + +static const char *TAG = "safety"; + static SemaphoreHandle_t xMutexAccessSafety = NULL; + +/** @brief Sensor sanity check configurations. */ static sSensorSanityCheck sanityChecks[NUMBER_OF_SENSOR_SANITY_CHECKS] = { {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); -void checkSensorSanity(void); -void setSafeState(void); +/* Private function prototypes */ +static void taskSafety(void *pvParameters); +static void checkSensorSanity(void); +static void setSafeState(void); -void initSafety(void) +esp_err_t initSafety(void) { xMutexAccessSafety = xSemaphoreCreateRecursiveMutex(); if (xMutexAccessSafety == NULL) { - ESP_LOGE(TAG, "Unable to create mutex"); + ESP_LOGE(TAG, "Failed to create mutex"); + return ESP_FAIL; } xSemaphoreGiveRecursive(xMutexAccessSafety); BaseType_t taskCreated = xTaskCreate( - taskSafety, // Function to implement the task - "taskSafety", // Task name - 4096, // Stack size (in words, not bytes) - NULL, // Parameters to the task function (none in this case) - 5, // Task priority (higher number = higher priority) - NULL // Task handle (optional) - ); + taskSafety, + "taskSafety", + 4096, + NULL, + 5, + NULL); - if (taskCreated == pdPASS) - { - ESP_LOGI(TAG, "Task created successfully!"); - } - else + if (taskCreated != pdPASS) { ESP_LOGE(TAG, "Failed to create task"); + return ESP_FAIL; } - setSafeState(); // Set inital state + setSafeState(); + + ESP_LOGI(TAG, "Initialized successfully"); + return ESP_OK; } -void taskSafety(void *pvParameters) +/** + * @brief Safety monitoring task. + * @param pvParameters Task parameters (unused). + */ +static void taskSafety(void *pvParameters) { while (1) { @@ -61,7 +81,6 @@ void taskSafety(void *pvParameters) if (xSemaphoreTakeRecursive(xMutexAccessSafety, portMAX_DELAY) == pdTRUE) { - checkSensorSanity(); if (sSafetyState != SAFETY_NO_ERROR) @@ -74,7 +93,10 @@ void taskSafety(void *pvParameters) } } -void checkSensorSanity(void) +/** + * @brief Check all sensor readings for sanity. + */ +static void checkSensorSanity(void) { sSafetyState = SAFETY_NO_ERROR; for (int i = 0; i < NUMBER_OF_SENSOR_SANITY_CHECKS; i++) @@ -130,7 +152,10 @@ void checkSensorSanity(void) } } -void setSafeState(void) +/** + * @brief Set system to safe state (burner off, pump on). + */ +static void setSafeState(void) { setCirculationPumpState(ENABLED); // To cool down system setBurnerState(DISABLED); // Deactivate burner diff --git a/main/safety.h b/main/safety.h index 58e6974..ad32cc5 100644 --- a/main/safety.h +++ b/main/safety.h @@ -1,56 +1,117 @@ +/** + * @file safety.h + * @brief Safety monitoring for temperature sensors. + * + * This module performs sanity checks on all temperature sensors and + * puts the system into a safe state if any sensor fails. + */ + #pragma once #include "outputs.h" #include "inputs.h" #include "sdkconfig.h" +#include "esp_err.h" #include +/** @brief Maximum length of sensor name string. */ #define MAX_ERROR_STRING_SIZE 64U + +/** @brief Number of sensors to monitor. */ #define NUMBER_OF_SENSOR_SANITY_CHECKS 4U +/** @brief Chamber sensor maximum temperature limit (°C). */ #define SENSOR_LIMIT_CHAMBER_MAX (CONFIG_SENSOR_LIMIT_CHAMBER_MAX / 10.0f) + +/** @brief Chamber sensor minimum temperature limit (°C). */ #define SENSOR_LIMIT_CHAMBER_MIN (CONFIG_SENSOR_LIMIT_CHAMBER_MIN / 10.0f) + +/** @brief Outdoor sensor maximum temperature limit (°C). */ #define SENSOR_LIMIT_OUTDOOR_MAX (CONFIG_SENSOR_LIMIT_OUTDOOR_MAX / 10.0f) + +/** @brief Outdoor sensor minimum temperature limit (°C). */ #define SENSOR_LIMIT_OUTDOOR_MIN (CONFIG_SENSOR_LIMIT_OUTDOOR_MIN / 10.0f) + +/** @brief Inlet flow sensor maximum temperature limit (°C). */ #define SENSOR_LIMIT_INLET_MAX (CONFIG_SENSOR_LIMIT_INLET_MAX / 10.0f) + +/** @brief Inlet flow sensor minimum temperature limit (°C). */ #define SENSOR_LIMIT_INLET_MIN (CONFIG_SENSOR_LIMIT_INLET_MIN / 10.0f) + +/** @brief Return flow sensor maximum temperature limit (°C). */ #define SENSOR_LIMIT_RETURN_MAX (CONFIG_SENSOR_LIMIT_RETURN_MAX / 10.0f) + +/** @brief Return flow sensor minimum temperature limit (°C). */ #define SENSOR_LIMIT_RETURN_MIN (CONFIG_SENSOR_LIMIT_RETURN_MIN / 10.0f) +/** + * @brief Sensor error state enumeration. + */ typedef enum _SensorErrorState { - SENSOR_NO_ERROR, - SENSOR_TOO_HIGH, - SENSOR_TOO_LOW, - SENSOR_UNCHANGED, - SENSOR_NOT_FOUND + SENSOR_NO_ERROR, /**< Sensor operating normally. */ + SENSOR_TOO_HIGH, /**< Temperature above maximum limit. */ + SENSOR_TOO_LOW, /**< Temperature below minimum limit. */ + SENSOR_UNCHANGED, /**< Temperature unchanged for too long. */ + SENSOR_NOT_FOUND /**< Sensor not responding. */ } eSensorErrorState; +/** + * @brief Overall safety state enumeration. + */ typedef enum _SafetyState { - SAFETY_NO_ERROR, - SAFETY_SENSOR_ERROR, - SAFETY_INTERNAL_ERROR + SAFETY_NO_ERROR, /**< All sensors OK. */ + SAFETY_SENSOR_ERROR, /**< At least one sensor failed. */ + SAFETY_INTERNAL_ERROR /**< Internal module error. */ } eSafetyState; +/** + * @brief Function pointer type for sensor getter functions. + */ typedef sMeasurement (*GetSensorValue)(); + +/** + * @brief Temperature sensor limits. + */ typedef struct _TemperatureSensorLimit { - float max; // Maximum temperature limit - float min; // Minimum temperature limit + float max; /**< Maximum temperature limit. */ + float min; /**< Minimum temperature limit. */ } sTemperatureSensorLimit; + +/** + * @brief Sensor sanity check state structure. + */ typedef struct _SensorSanityCheck { - eSensorErrorState state; - char name[MAX_ERROR_STRING_SIZE]; - sTemperatureSensorLimit sSensorLimit; - float fSensorTemperatureLast; - uint32_t uUnchangedCounter; - GetSensorValue getSensor; + eSensorErrorState state; /**< Current error state. */ + char name[MAX_ERROR_STRING_SIZE]; /**< Sensor name for logging. */ + sTemperatureSensorLimit sSensorLimit; /**< Temperature limits. */ + float fSensorTemperatureLast; /**< Last temperature reading. */ + uint32_t uUnchangedCounter; /**< Counter for unchanged readings. */ + GetSensorValue getSensor; /**< Function to get sensor value. */ } sSensorSanityCheck; -void initSafety(void); +/** + * @brief Initialize the safety module. + * + * Creates the safety monitoring task and sets initial safe state. + * + * @return ESP_OK on success, ESP_FAIL on error. + */ +esp_err_t initSafety(void); + +/** + * @brief Get the current sensor sanity states. + * @param[out] pSensorSanityChecks Array to receive sensor states. + */ void getSensorSanityStates(sSensorSanityCheck *pSensorSanityChecks); -eSafetyState getSafetyState(void); \ No newline at end of file + +/** + * @brief Get the overall safety state. + * @return eSafetyState indicating current safety status. + */ +eSafetyState getSafetyState(void); diff --git a/main/sntp.c b/main/sntp.c index f00bdd6..e9c60ca 100644 --- a/main/sntp.c +++ b/main/sntp.c @@ -1,3 +1,8 @@ +/** + * @file sntp.c + * @brief Implementation of SNTP client module. + */ + #include "sntp.h" #include "esp_sntp.h" @@ -6,17 +11,22 @@ #include #include -static const char *TAG = "smart-oil-heater-control-system-sntp"; -static volatile eSntpState sntpState = SYNC_NOT_STARTED; -void time_sync_notification_cb(struct timeval *tv); +static const char *TAG = "sntp"; -void initSntp(void) +static volatile eSntpState sntpState = SYNC_NOT_STARTED; + +static void time_sync_notification_cb(struct timeval *tv); + +esp_err_t initSntp(void) { esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); esp_sntp_setservername(0, CONFIG_SNTP_SERVER_IP_ADDR); sntp_set_time_sync_notification_cb(time_sync_notification_cb); esp_sntp_init(); + + ESP_LOGI(TAG, "Initialized successfully, server: %s", CONFIG_SNTP_SERVER_IP_ADDR); + return ESP_OK; } eSntpState getSntpState(void) @@ -24,8 +34,12 @@ eSntpState getSntpState(void) return sntpState; } -void time_sync_notification_cb(struct timeval *tv) +/** + * @brief SNTP time sync callback. + * @param tv Synchronized time value. + */ +static void time_sync_notification_cb(struct timeval *tv) { - ESP_LOGI(TAG, "SNTP synchronization! Unix Time: %lld", tv->tv_sec); + ESP_LOGI(TAG, "Time synchronized! Unix Time: %lld", tv->tv_sec); sntpState = SYNC_SUCCESSFUL; } diff --git a/main/sntp.h b/main/sntp.h index 4feb88b..1967234 100644 --- a/main/sntp.h +++ b/main/sntp.h @@ -1,11 +1,37 @@ +/** + * @file sntp.h + * @brief SNTP client for time synchronization. + * + * This module synchronizes system time with an NTP server. + * Time sync is required for schedule-based heating control. + */ + #pragma once +#include "esp_err.h" + +/** + * @brief SNTP synchronization state enumeration. + */ typedef enum _SntpState { - SYNC_SUCCESSFUL, - SYNC_NOT_STARTED, - SYNC_FAILED, + SYNC_SUCCESSFUL, /**< Time synchronized successfully. */ + SYNC_NOT_STARTED, /**< Synchronization not yet attempted. */ + SYNC_FAILED, /**< Synchronization failed. */ } eSntpState; -void initSntp(void); -eSntpState getSntpState(void); \ No newline at end of file +/** + * @brief Initialize the SNTP client. + * + * Configures SNTP with the server from Kconfig and starts + * periodic time synchronization. + * + * @return ESP_OK on success, ESP_FAIL on error. + */ +esp_err_t initSntp(void); + +/** + * @brief Get the current SNTP synchronization state. + * @return eSntpState indicating sync status. + */ +eSntpState getSntpState(void); diff --git a/main/wifi.c b/main/wifi.c index bf764d4..ace3e96 100644 --- a/main/wifi.c +++ b/main/wifi.c @@ -1,3 +1,8 @@ +/** + * @file wifi.c + * @brief Implementation of WiFi station mode module. + */ + #include "wifi.h" #include "esp_timer.h" @@ -12,12 +17,19 @@ #include +/** @brief Event bit for successful connection. */ #define WIFI_CONNECTED_BIT BIT0 + +/** @brief Event bit for connection failure. */ #define WIFI_FAIL_BIT BIT1 + +/** @brief Maximum connection retry attempts. */ #define MAX_RETRY_COUNT 10 + +/** @brief Delay between retries in milliseconds. */ #define RETRY_DELAY_MS 1000 -static const char *TAG = "smart-oil-heater-control-system-wifi"; +static const char *TAG = "wifi"; static EventGroupHandle_t s_wifi_event_group; static int s_retry_num = 0; @@ -26,13 +38,13 @@ 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); -void initWifi(void) +esp_err_t initWifi(void) { s_wifi_event_group = xEventGroupCreate(); if (s_wifi_event_group == NULL) { - ESP_LOGE(TAG, "xEventGroupCreate() failed!"); - return; + ESP_LOGE(TAG, "Failed to create event group"); + return ESP_FAIL; } ESP_ERROR_CHECK(esp_netif_init()); @@ -73,10 +85,10 @@ void initWifi(void) ESP_ERROR_CHECK(esp_wifi_start()); - ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(78)); // Set max power to 19.5 dBm (78 in units of 0.25 dBm) - ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); // Use power-saving mode + ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(78)); + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); - ESP_LOGI(TAG, "wifi_init_sta finished."); + ESP_LOGI(TAG, "WiFi init finished, waiting for connection..."); EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, @@ -86,21 +98,32 @@ void initWifi(void) if (bits & WIFI_CONNECTED_BIT) { - ESP_LOGI(TAG, "Connected to ap SSID:%s", CONFIG_SSID); + ESP_LOGI(TAG, "Connected to AP SSID:%s", CONFIG_SSID); } else if (bits & WIFI_FAIL_BIT) { - ESP_LOGI(TAG, "Failed to connect to SSID:%s", CONFIG_SSID); + ESP_LOGE(TAG, "Failed to connect to SSID:%s", CONFIG_SSID); + return ESP_FAIL; } else { ESP_LOGE(TAG, "Unexpected event"); + return ESP_FAIL; } - // Mark initial connection phase complete - do NOT delete the event group s_initial_connect = false; + + ESP_LOGI(TAG, "Initialized successfully"); + return ESP_OK; } +/** + * @brief WiFi event handler. + * @param arg User argument (unused). + * @param event_base Event base. + * @param event_id Event ID. + * @param event_data Event data. + */ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { diff --git a/main/wifi.h b/main/wifi.h index 380c625..a4ad953 100644 --- a/main/wifi.h +++ b/main/wifi.h @@ -1,3 +1,22 @@ +/** + * @file wifi.h + * @brief WiFi station mode initialization and management. + * + * This module initializes WiFi in station mode with static IP + * configuration. It handles connection and automatic reconnection. + */ + #pragma once -void initWifi(void); \ No newline at end of file +#include "esp_err.h" + +/** + * @brief Initialize WiFi in station mode. + * + * Configures WiFi with static IP address from Kconfig settings + * and connects to the configured access point. Blocks until + * connected or maximum retry count is reached. + * + * @return ESP_OK on success, ESP_FAIL on error. + */ +esp_err_t initWifi(void); -- 2.50.1 From 430b4cb69072f80ca2ed7e86f63474db2f5b08851fc545664659e5e9472d1a03 Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 13:42:24 +0100 Subject: [PATCH 20/21] cleanup README --- README.md | 101 +++++++++++++----------------------------------------- 1 file changed, 23 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 07d79a3..04a79af 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,13 @@ -# smart-oil-heating-control-system +# Smart Oil Heating Control System -## Software -### Design +ESP32-based control system for oil-fired central heating with schedule-based temperature management, safety monitoring, and Prometheus metrics export. -```mermaid -classDiagram -Inputs <|-- Control -Outputs <|-- Control -Sntp <|-- Control -Inputs <|-- Safety -Outputs <|--|> Safety +## Features -Inputs <|-- Metrics -Outputs <|-- Metrics -Control <|-- Metrics -Safety <|-- Metrics -Sntp <|-- Metrics - - class Inputs{ - +initInputs() - -initMeasurement() - -updateAverage() - -updatePrediction() - -taskInput() - -linearRegressionPredict() - +getChamberTemperature() - +getOutdoorTemperature() - +getInletFlowTemperature() - +getReturnFlowTemperature() - +getBurnerError() - } - - class Outputs{ - +initOutputs() - +getCirculationPumpState() - +setCirculationPumpState() - +getBurnerState() - +setBurnerState() - +getSafetyControlState() - +setSafetyControlState() - } - - class Control{ - initControl() - +taskControl() - +getControlCurrentWeekday() - -findControlCurrentTemperatureEntry() - +getControlCurrentTemperatureEntry() - -controlTable - +getControlState() - } - - class Safety{ - +initSafety() - -taskSafety() - -setSafeState() - -checkSensorSanity() - +getSensorSanityStates() - +getSafetyState() - } - - - class Wifi{ - +initWifi() - } - - class Sntp{ - +initSntp() - +getSntpState() - } - - class Metrics{ - +initMetrics() - -taskMetrics() - -metrics - +event_handler() - +connect_wifi() - +setMetrics() - } -``` +- **Schedule Control**: Day/night temperature targets per weekday +- **Summer Mode**: Automatic heating disable based on outdoor temperature +- **Safety Monitoring**: Sensor sanity checks with automatic safe-state fallback +- **Prometheus Metrics**: HTTP endpoint at port 9100 ### Prometheus Metrics `curl http://X.X.X.X:9100/metrics` @@ -180,4 +109,20 @@ wifi_rssi -72 | Input Burner Fault | IO19 | Digital Input IN1 | | Input Temperature DS10B20 | IO04 | 1-Wire | +## Configuration +All parameters configurable via `idf.py menuconfig`: +- WiFi credentials and static IP +- GPIO pin assignments +- 1-Wire sensor addresses +- Temperature thresholds and limits +- Heating schedule (day/night per weekday) +- Damping factors + +## Building + +```bash +idf.py set-target esp32 +idf.py menuconfig # Configure settings +idf.py build flash monitor +``` \ No newline at end of file -- 2.50.1 From 4f355bdfdfdffa6ccb9726c6563f5f2b4d3aa35837785e1aa22119ac2404082e Mon Sep 17 00:00:00 2001 From: localhorst Date: Sat, 10 Jan 2026 18:51:33 +0100 Subject: [PATCH 21/21] update README --- README.md | 77 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 04a79af..845fde9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,35 @@ ESP32-based control system for oil-fired central heating with schedule-based tem - **Safety Monitoring**: Sensor sanity checks with automatic safe-state fallback - **Prometheus Metrics**: HTTP endpoint at port 9100 +## System Overview +```mermaid +flowchart TB + subgraph OUTSIDE[" "] + OT[/"🌡️ Outdoor Temp
DS18B20"/] + end + + subgraph BURNER["OIL BURNER"] + CT[/"🌡️ Chamber Temp
DS18B20"/] + BF[["⚠️ Burner Fault
GPIO19 INPUT"]] + BR(["🔥 Burner Relay
GPIO14"]) + SC(["🔌 Safety Contact
GPIO12"]) + end + + subgraph CIRCUIT["HEATING CIRCUIT"] + IT[/"🌡️ Inlet Temp
DS18B20"/] + CP(["💧 Circulation Pump
GPIO27"]) + RT[/"🌡️ Return Temp
DS18B20"/] + end + + RAD["🏠 Radiators"] + + BURNER -->|"hot water"| IT + IT --> CP + CP --> RAD + RAD --> RT + RT -->|"cold water"| BURNER +``` + ### Prometheus Metrics `curl http://X.X.X.X:9100/metrics` #### Example @@ -17,26 +46,26 @@ burner_fault_pending 1 circulation_pump_enabled 1 burner_enabled 0 safety_contact_enabled 1 -chamber_temperature 37.250000 -chamber_temperature_avg10 37.237499 -chamber_temperature_avg60 37.438541 -chamber_temperature_damped 42.185040 -chamber_temperature_pred60 36.638443 -inlet_flow_temperature 35.625000 -inlet_flow_temperature_avg10 35.618752 -inlet_flow_temperature_avg60 35.415627 -inlet_flow_temperature_damped 39.431259 -inlet_flow_temperature_pred60 36.078678 -outdoor_temperature 14.687500 -outdoor_temperature_avg10 14.662500 -outdoor_temperature_avg60 14.646875 -outdoor_temperature_damped 9.169084 -outdoor_temperature_pred60 14.660233 -return_flow_temperature 39.937500 -return_flow_temperature_avg10 40.087502 -return_flow_temperature_avg60 41.146873 -return_flow_temperature_damped 32.385151 -return_flow_temperature_pred60 37.311958 +chamber_temperature 37.312500 +chamber_temperature_avg10 37.393749 +chamber_temperature_avg60 37.689583 +chamber_temperature_damped 38.058098 +chamber_temperature_pred60 36.697266 +inlet_flow_temperature 34.562500 +inlet_flow_temperature_avg10 34.587502 +inlet_flow_temperature_avg60 34.880207 +inlet_flow_temperature_damped 35.255993 +inlet_flow_temperature_pred60 33.910374 +outdoor_temperature 1.812500 +outdoor_temperature_avg10 1.825000 +outdoor_temperature_avg60 1.821875 +outdoor_temperature_damped 2.390663 +outdoor_temperature_pred60 1.840263 +return_flow_temperature 34.125000 +return_flow_temperature_avg10 34.162498 +return_flow_temperature_avg60 34.304165 +return_flow_temperature_damped 31.430506 +return_flow_temperature_pred60 33.858772 chamber_temperature_state 0 outdoor_temperature_state 0 inlet_flow_temperature_state 0 @@ -44,13 +73,13 @@ return_flow_temperature_state 0 safety_state 0 control_state 3 control_current_weekday 5 -control_current_entry_time 17100 +control_current_entry_time 24300 control_current_entry_chamber_temperature 80.000000 control_current_entry_return_flow_temperature 30.000000 sntp_state 0 -system_unixtime 1762012743 -uptime_seconds 465229 -wifi_rssi -72 +system_unixtime 1768067412 +uptime_seconds 344878 +wifi_rssi -59 ``` #### Status Encoding -- 2.50.1