3 Commits

Author SHA256 Message Date
4f355bdfdf update README 2026-01-10 18:51:33 +01:00
430b4cb690 cleanup README 2026-01-10 13:42:24 +01:00
1d4e272d80 error handling and cleanup 2026-01-10 13:32:49 +01:00
16 changed files with 938 additions and 406 deletions

168
README.md
View File

@ -1,83 +1,41 @@
# smart-oil-heating-control-system # Smart Oil Heating Control System
## Software ESP32-based control system for oil-fired central heating with schedule-based temperature management, safety monitoring, and Prometheus metrics export.
### Design
## Features
- **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
## System Overview
```mermaid ```mermaid
classDiagram flowchart TB
Inputs <|-- Control subgraph OUTSIDE[" "]
Outputs <|-- Control OT[/"🌡️ Outdoor Temp<br/>DS18B20"/]
Sntp <|-- Control end
Inputs <|-- Safety
Outputs <|--|> Safety
Inputs <|-- Metrics subgraph BURNER["OIL BURNER"]
Outputs <|-- Metrics CT[/"🌡️ Chamber Temp<br/>DS18B20"/]
Control <|-- Metrics BF[["⚠️ Burner Fault<br/>GPIO19 INPUT"]]
Safety <|-- Metrics BR(["🔥 Burner Relay<br/>GPIO14"])
Sntp <|-- Metrics SC(["🔌 Safety Contact<br/>GPIO12"])
end
class Inputs{ subgraph CIRCUIT["HEATING CIRCUIT"]
+initInputs() IT[/"🌡️ Inlet Temp<br/>DS18B20"/]
-initMeasurement() CP(["💧 Circulation Pump<br/>GPIO27"])
-updateAverage() RT[/"🌡️ Return Temp<br/>DS18B20"/]
-updatePrediction() end
-taskInput()
-linearRegressionPredict()
+getChamberTemperature()
+getOutdoorTemperature()
+getInletFlowTemperature()
+getReturnFlowTemperature()
+getBurnerError()
}
class Outputs{ RAD["🏠 Radiators"]
+initOutputs()
+getCirculationPumpState()
+setCirculationPumpState()
+getBurnerState()
+setBurnerState()
+getSafetyControlState()
+setSafetyControlState()
}
class Control{ BURNER -->|"hot water"| IT
initControl() IT --> CP
+taskControl() CP --> RAD
+getControlCurrentWeekday() RAD --> RT
-findControlCurrentTemperatureEntry() RT -->|"cold water"| BURNER
+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()
}
``` ```
### Prometheus Metrics ### Prometheus Metrics
@ -88,26 +46,26 @@ burner_fault_pending 1
circulation_pump_enabled 1 circulation_pump_enabled 1
burner_enabled 0 burner_enabled 0
safety_contact_enabled 1 safety_contact_enabled 1
chamber_temperature 37.250000 chamber_temperature 37.312500
chamber_temperature_avg10 37.237499 chamber_temperature_avg10 37.393749
chamber_temperature_avg60 37.438541 chamber_temperature_avg60 37.689583
chamber_temperature_damped 42.185040 chamber_temperature_damped 38.058098
chamber_temperature_pred60 36.638443 chamber_temperature_pred60 36.697266
inlet_flow_temperature 35.625000 inlet_flow_temperature 34.562500
inlet_flow_temperature_avg10 35.618752 inlet_flow_temperature_avg10 34.587502
inlet_flow_temperature_avg60 35.415627 inlet_flow_temperature_avg60 34.880207
inlet_flow_temperature_damped 39.431259 inlet_flow_temperature_damped 35.255993
inlet_flow_temperature_pred60 36.078678 inlet_flow_temperature_pred60 33.910374
outdoor_temperature 14.687500 outdoor_temperature 1.812500
outdoor_temperature_avg10 14.662500 outdoor_temperature_avg10 1.825000
outdoor_temperature_avg60 14.646875 outdoor_temperature_avg60 1.821875
outdoor_temperature_damped 9.169084 outdoor_temperature_damped 2.390663
outdoor_temperature_pred60 14.660233 outdoor_temperature_pred60 1.840263
return_flow_temperature 39.937500 return_flow_temperature 34.125000
return_flow_temperature_avg10 40.087502 return_flow_temperature_avg10 34.162498
return_flow_temperature_avg60 41.146873 return_flow_temperature_avg60 34.304165
return_flow_temperature_damped 32.385151 return_flow_temperature_damped 31.430506
return_flow_temperature_pred60 37.311958 return_flow_temperature_pred60 33.858772
chamber_temperature_state 0 chamber_temperature_state 0
outdoor_temperature_state 0 outdoor_temperature_state 0
inlet_flow_temperature_state 0 inlet_flow_temperature_state 0
@ -115,13 +73,13 @@ return_flow_temperature_state 0
safety_state 0 safety_state 0
control_state 3 control_state 3
control_current_weekday 5 control_current_weekday 5
control_current_entry_time 17100 control_current_entry_time 24300
control_current_entry_chamber_temperature 80.000000 control_current_entry_chamber_temperature 80.000000
control_current_entry_return_flow_temperature 30.000000 control_current_entry_return_flow_temperature 30.000000
sntp_state 0 sntp_state 0
system_unixtime 1762012743 system_unixtime 1768067412
uptime_seconds 465229 uptime_seconds 344878
wifi_rssi -72 wifi_rssi -59
``` ```
#### Status Encoding #### Status Encoding
@ -180,4 +138,20 @@ wifi_rssi -72
| Input Burner Fault | IO19 | Digital Input IN1 | | Input Burner Fault | IO19 | Digital Input IN1 |
| Input Temperature DS10B20 | IO04 | 1-Wire | | 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
```

View File

@ -1,3 +1,8 @@
/**
* @file control.c
* @brief Implementation of heating control module.
*/
#include "control.h" #include "control.h"
#include "inputs.h" #include "inputs.h"
#include "outputs.h" #include "outputs.h"
@ -9,11 +14,16 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#define PERIODIC_INTERVAL 1U // Run control loop every 1 second #include <time.h>
/** @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; static eControlState gControlState = CONTROL_STARTING;
// Control table for daily schedules
/** @brief Weekly schedule table (from Kconfig). */
static const sControlDay gControlTable[] = { static const sControlDay gControlTable[] = {
{MONDAY, {MONDAY,
2U, 2U,
@ -72,45 +82,49 @@ static const sControlDay gControlTable[] = {
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
}; };
static sControlTemperatureEntry gCurrentControlEntry = static sControlTemperatureEntry gCurrentControlEntry =
gControlTable[0].aTemperatureEntries[0]; gControlTable[0].aTemperatureEntries[0];
static SemaphoreHandle_t xMutexAccessControl = NULL; static SemaphoreHandle_t xMutexAccessControl = NULL;
// Function prototypes /* Private function prototypes */
void taskControl(void *pvParameters); static void taskControl(void *pvParameters);
void findControlCurrentTemperatureEntry(void); static void findControlCurrentTemperatureEntry(void);
void setControlState(eControlState state); static void setControlState(eControlState state);
void initControl(void) esp_err_t initControl(void)
{ {
xMutexAccessControl = xSemaphoreCreateRecursiveMutex(); xMutexAccessControl = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessControl == NULL) if (xMutexAccessControl == NULL)
{ {
ESP_LOGE(TAG, "Unable to create mutex"); ESP_LOGE(TAG, "Failed to create mutex");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessControl); xSemaphoreGiveRecursive(xMutexAccessControl);
BaseType_t taskCreated = BaseType_t taskCreated = xTaskCreate(
xTaskCreate(taskControl, // Function to implement the task taskControl,
"taskControl", // Task name "taskControl",
8192, // Stack size (in words, not bytes) 8192,
NULL, // Parameters to the task function (none in this case) NULL,
5, // Task priority (higher number = higher priority) 5,
NULL // Task handle (optional) NULL);
);
if (taskCreated == pdPASS) if (taskCreated != pdPASS)
{
ESP_LOGI(TAG, "Task created successfully!");
}
else
{ {
ESP_LOGE(TAG, "Failed to create task"); 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 bHeatingInAction = false;
bool bSummerMode = false; bool bSummerMode = false;
@ -121,7 +135,7 @@ void taskControl(void *pvParameters)
{ {
vTaskDelay(PERIODIC_INTERVAL * 1000U / portTICK_PERIOD_MS); vTaskDelay(PERIODIC_INTERVAL * 1000U / portTICK_PERIOD_MS);
// Check for safety faults /* Check for safety faults */
if (getSafetyState() != SAFETY_NO_ERROR) if (getSafetyState() != SAFETY_NO_ERROR)
{ {
ESP_LOGW(TAG, "Control not possible due to safety fault!"); ESP_LOGW(TAG, "Control not possible due to safety fault!");
@ -136,7 +150,7 @@ void taskControl(void *pvParameters)
continue; continue;
} }
// Check for SNTP faults /* Check for SNTP faults */
if (getSntpState() != SYNC_SUCCESSFUL) if (getSntpState() != SYNC_SUCCESSFUL)
{ {
ESP_LOGW(TAG, "Control not possible due to SNTP fault!"); ESP_LOGW(TAG, "Control not possible due to SNTP fault!");
@ -153,35 +167,30 @@ void taskControl(void *pvParameters)
findControlCurrentTemperatureEntry(); findControlCurrentTemperatureEntry();
if (getOutdoorTemperature().fDampedValue >= /* Summer mode hysteresis */
SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH) if (getOutdoorTemperature().fDampedValue >= SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH)
{ {
bSummerMode = true; bSummerMode = true;
} }
else if (getOutdoorTemperature().fDampedValue <= else if (getOutdoorTemperature().fDampedValue <= SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW)
SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW)
{ {
bSummerMode = false; bSummerMode = false;
} }
// Enable burner if outdoor temperature is low and return flow temperature /* Enable burner if needed */
// is cooled down
if (!bHeatingInAction && (burnerState != BURNER_FAULT)) if (!bHeatingInAction && (burnerState != BURNER_FAULT))
{ {
if (bSummerMode) if (bSummerMode)
{ {
// ESP_LOGI(TAG, "Outdoor temperature too warm: Disabling heating");
setBurnerState(DISABLED); setBurnerState(DISABLED);
setSafetyControlState(DISABLED); setSafetyControlState(DISABLED);
setControlState(CONTROL_OUTDOOR_TOO_WARM); setControlState(CONTROL_OUTDOOR_TOO_WARM);
} }
else if ((getReturnFlowTemperature().average60s.fValue <= else if ((getReturnFlowTemperature().average60s.fValue <=
getControlCurrentTemperatureEntry().fReturnFlowTemperature) && getControlCurrentTemperatureEntry().fReturnFlowTemperature) &&
(getChamberTemperature().fCurrentValue <= (getChamberTemperature().fCurrentValue <= CHAMBER_TEMPERATURE_THRESHOLD))
CHAMBER_TEMPERATURE_THRESHOLD))
{ {
ESP_LOGI(TAG, ESP_LOGI(TAG, "Enabling burner: Return flow temperature target reached");
"Enabling burner: Return flow temperature target reached");
burnerState = BURNER_UNKNOWN; burnerState = BURNER_UNKNOWN;
bHeatingInAction = true; bHeatingInAction = true;
setBurnerState(ENABLED); setBurnerState(ENABLED);
@ -191,12 +200,11 @@ void taskControl(void *pvParameters)
} }
else else
{ {
// ESP_LOGI(TAG, "Return flow temperature too warm: Disabling heating");
setControlState(CONTROL_RETURN_FLOW_TOO_WARM); 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 (bHeatingInAction)
{ {
if ((getChamberTemperature().fCurrentValue >= if ((getChamberTemperature().fCurrentValue >=
@ -233,9 +241,8 @@ void taskControl(void *pvParameters)
} }
} }
// Manage circulation pump /* Manage circulation pump */
if (getChamberTemperature().fCurrentValue <= if (getChamberTemperature().fCurrentValue <= CIRCULATION_PUMP_TEMPERATURE_THRESHOLD)
CIRCULATION_PUMP_TEMPERATURE_THRESHOLD)
{ {
// ESP_LOGI(TAG, "Burner cooled down: Disabling circulation pump"); // ESP_LOGI(TAG, "Burner cooled down: Disabling circulation pump");
setCirculationPumpState(DISABLED); setCirculationPumpState(DISABLED);
@ -248,9 +255,12 @@ void taskControl(void *pvParameters)
} // End of while(1) } // 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) if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE)
{ {
gControlState = state; gControlState = state;
@ -264,7 +274,6 @@ void setControlState(eControlState state)
eControlState getControlState(void) eControlState getControlState(void)
{ {
eControlState ret = CONTROL_FAULT_SAFETY; eControlState ret = CONTROL_FAULT_SAFETY;
if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE) if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE)
@ -282,7 +291,6 @@ eControlState getControlState(void)
eControlWeekday getControlCurrentWeekday(void) eControlWeekday getControlCurrentWeekday(void)
{ {
// Get current time
time_t now; time_t now;
struct tm timeinfo; struct tm timeinfo;
time(&now); time(&now);
@ -293,24 +301,12 @@ eControlWeekday getControlCurrentWeekday(void)
} }
/** /**
* @brief Finds the active temperature control entry for the current time. * @brief Find the currently active temperature entry based on 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.
*/ */
/** static void findControlCurrentTemperatureEntry(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.
*/
void findControlCurrentTemperatureEntry(void)
{ {
eControlWeekday currentDay = getControlCurrentWeekday(); eControlWeekday currentDay = getControlCurrentWeekday();
// Get current time
time_t now; time_t now;
struct tm timeinfo; struct tm timeinfo;
time(&now); time(&now);
@ -361,14 +357,15 @@ void findControlCurrentTemperatureEntry(void)
const sControlDay *sunday = &gControlTable[6]; const sControlDay *sunday = &gControlTable[6];
gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1]; gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
} }
xSemaphoreGiveRecursive(xMutexAccessControl);
/* /*
ESP_LOGI(TAG, "Active entry found - Time: %02d:%02d, " ESP_LOGI(TAG, "Active entry found - Time: %02d:%02d, "
"Return Temp: %lf, Chamber Temp: %lf", "Return Temp: %lf, Chamber Temp: %lf",
gCurrentControlEntry.timestamp.hour, gCurrentControlEntry.timestamp.hour,
gCurrentControlEntry.timestamp.minute, gCurrentControlEntry.timestamp.minute,
gCurrentControlEntry.fReturnFlowTemperature, gCurrentControlEntry.fReturnFlowTemperature,
gCurrentControlEntry.fChamberTemperature); gCurrentControlEntry.fChamberTemperature);
*/ */
return; return;
} }
} }

View File

@ -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 #pragma once
#include "sdkconfig.h" #include "sdkconfig.h"
#include "esp_err.h"
#include <time.h> #include <stdint.h>
#include <stddef.h>
/** @brief Maximum number of temperature entries per day. */
#define MAX_TEMPERATURE_ENTRIES_PER_DAY 24U #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) #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) #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) #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) #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) #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) #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) #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 #define BURNER_FAULT_DETECTION_THRESHOLD CONFIG_BURNER_FAULT_DETECTION_SECONDS
/**
* @brief Control state enumeration.
*/
typedef enum _ControlState typedef enum _ControlState
{ {
CONTROL_STARTING, CONTROL_STARTING, /**< System starting up. */
CONTROL_HEATING, CONTROL_HEATING, /**< Burner running. */
CONTROL_OUTDOOR_TOO_WARM, CONTROL_OUTDOOR_TOO_WARM, /**< Summer mode active. */
CONTROL_RETURN_FLOW_TOO_WARM, CONTROL_RETURN_FLOW_TOO_WARM, /**< Target temperature reached. */
CONTROL_FAULT_BURNER, CONTROL_FAULT_BURNER, /**< Burner fault detected. */
CONTROL_FAULT_SAFETY, CONTROL_FAULT_SAFETY, /**< Safety fault detected. */
CONTROL_FAULT_SNTP, CONTROL_FAULT_SNTP, /**< SNTP sync failed. */
} eControlState; } eControlState;
/**
* @brief Burner operational state enumeration.
*/
typedef enum _BurnerState typedef enum _BurnerState
{ {
BURNER_UNKNOWN, // Burner is disabled or state after enabling is still unkown BURNER_UNKNOWN, /**< Burner state unknown after enable. */
BURNER_FIRED, // Burner fired successfully BURNER_FIRED, /**< Burner fired successfully. */
BURNER_FAULT // Burner was unable to fire successfully BURNER_FAULT /**< Burner failed to fire. */
} eBurnerState; } eBurnerState;
/**
* @brief Weekday enumeration (Monday = 0).
*/
typedef enum _ControlWeekday typedef enum _ControlWeekday
{ {
MONDAY, MONDAY,
@ -44,27 +80,58 @@ typedef enum _ControlWeekday
SUNDAY, SUNDAY,
} eControlWeekday; } eControlWeekday;
/**
* @brief Time of day structure.
*/
typedef struct _ControlTimestamp typedef struct _ControlTimestamp
{ {
uint8_t hour; uint8_t hour; /**< Hour (0-23). */
uint8_t minute; uint8_t minute; /**< Minute (0-59). */
} sControlTimestamp; } sControlTimestamp;
/**
* @brief Temperature schedule entry.
*/
typedef struct _ControlTemperatureEntry typedef struct _ControlTemperatureEntry
{ {
sControlTimestamp timestamp; sControlTimestamp timestamp; /**< Time when entry becomes active. */
float fReturnFlowTemperature; float fReturnFlowTemperature; /**< Target return flow temperature. */
float fChamberTemperature; float fChamberTemperature; /**< Target chamber temperature. */
} sControlTemperatureEntry; } sControlTemperatureEntry;
/**
* @brief Daily schedule structure.
*/
typedef struct _ControlDay typedef struct _ControlDay
{ {
eControlWeekday day; eControlWeekday day; /**< Day of week. */
size_t entryCount; // number of entries for each day size_t entryCount; /**< Number of entries for this day. */
sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY]; sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY]; /**< Schedule entries. */
} sControlDay; } 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); eControlState getControlState(void);
/**
* @brief Get the current weekday.
* @return eControlWeekday (Monday = 0, Sunday = 6).
*/
eControlWeekday getControlCurrentWeekday(void); eControlWeekday getControlCurrentWeekday(void);
/**
* @brief Get the currently active temperature entry.
* @return sControlTemperatureEntry with current targets.
*/
sControlTemperatureEntry getControlCurrentTemperatureEntry(void); sControlTemperatureEntry getControlCurrentTemperatureEntry(void);

View File

@ -1,3 +1,8 @@
/**
* @file inputs.c
* @brief Implementation of input handling module.
*/
#include "inputs.h" #include "inputs.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
@ -9,22 +14,38 @@
#include <string.h> #include <string.h>
#include <math.h> #include <math.h>
/** @brief Maximum number of DS18B20 sensors supported. */
#define MAX_DN18B20_SENSORS 4U #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"; /** @brief Number of retry attempts for 1-Wire read. */
const uint8_t uBurnerFaultPin = CONFIG_GPIO_BURNER_FAULT; #define ONE_WIRE_LOOPS 4U
const uint8_t uDS18B20Pin = CONFIG_GPIO_DS18B20_ONEWIRE;
const onewire_addr_t uChamperTempSensorAddr = CONFIG_ONEWIRE_ADDR_CHAMBER_TEMP; /** @brief Task interval in seconds. */
const onewire_addr_t uOutdoorTempSensorAddr = CONFIG_ONEWIRE_ADDR_OUTDOOR_TEMP; #define PERIODIC_INTERVAL 1U
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]; static const char *TAG = "inputs";
float fDS18B20Temps[MAX_DN18B20_SENSORS];
size_t sSensorCount = 0U; /** @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 SemaphoreHandle_t xMutexAccessInputs = NULL;
static eBurnerErrorState sBurnerErrorState; static eBurnerErrorState sBurnerErrorState;
@ -33,34 +54,34 @@ static sMeasurement sOutdoorTemperature;
static sMeasurement sInletFlowTemperature; static sMeasurement sInletFlowTemperature;
static sMeasurement sReturnFlowTemperature; static sMeasurement sReturnFlowTemperature;
void taskInput(void *pvParameters); /* Private function prototypes */
void initMeasurement(sMeasurement *pMeasurement); static void taskInput(void *pvParameters);
void updateAverage(sMeasurement *pMeasurement); static void initMeasurement(sMeasurement *pMeasurement);
void updatePrediction(sMeasurement *pMeasurement); static void updateAverage(sMeasurement *pMeasurement);
float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex); 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 = { gpio_config_t ioConfBurnerFault = {
.pin_bit_mask = (1ULL << uBurnerFaultPin), // Pin mask .pin_bit_mask = (1ULL << uBurnerFaultPin),
.mode = GPIO_MODE_INPUT, // Set as inout .mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE, // Enable pull-up .pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down .pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE // Disable interrupts .intr_type = GPIO_INTR_DISABLE};
};
esp_err_t ret = gpio_config(&ioConfBurnerFault); esp_err_t ret = gpio_config(&ioConfBurnerFault);
if (ret != ESP_OK) if (ret != ESP_OK)
{ {
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret));
return; return ESP_FAIL;
} }
xMutexAccessInputs = xSemaphoreCreateRecursiveMutex(); xMutexAccessInputs = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessInputs == NULL) if (xMutexAccessInputs == NULL)
{ {
ESP_LOGE(TAG, "Unable to create mutex"); ESP_LOGE(TAG, "Failed to create mutex");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessInputs); xSemaphoreGiveRecursive(xMutexAccessInputs);
@ -70,25 +91,28 @@ void initInputs(void)
initMeasurement(&sReturnFlowTemperature); initMeasurement(&sReturnFlowTemperature);
BaseType_t taskCreated = xTaskCreate( BaseType_t taskCreated = xTaskCreate(
taskInput, // Function to implement the task taskInput,
"taskInput", // Task name "taskInput",
4096, // Stack size (in words, not bytes) 4096,
NULL, // Parameters to the task function (none in this case) NULL,
5, // Task priority (higher number = higher priority) 5,
NULL // Task handle (optional) NULL);
);
if (taskCreated == pdPASS) if (taskCreated != pdPASS)
{
ESP_LOGI(TAG, "Task created successfully!");
}
else
{ {
ESP_LOGE(TAG, "Failed to create task"); 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) if (!pMeasurement)
return; return;
@ -113,12 +137,16 @@ void initMeasurement(sMeasurement *pMeasurement)
memset(pMeasurement->predict60s.samples, 0U, sizeof(float) * PRED60S_SAMPLE_SIZE); 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) if (!pMeasurement)
return; return;
// Average form the last 10sec /* 10-second average */
pMeasurement->average10s.samples[pMeasurement->average10s.bufferIndex] = pMeasurement->fCurrentValue; pMeasurement->average10s.samples[pMeasurement->average10s.bufferIndex] = pMeasurement->fCurrentValue;
pMeasurement->average10s.bufferIndex = (pMeasurement->average10s.bufferIndex + 1) % AVG10S_SAMPLE_SIZE; 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; 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.samples[pMeasurement->average60s.bufferIndex] = pMeasurement->fCurrentValue;
pMeasurement->average60s.bufferIndex = (pMeasurement->average60s.bufferIndex + 1) % AVG60S_SAMPLE_SIZE; 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; pMeasurement->average60s.fValue = sum / pMeasurement->average60s.bufferCount;
} }
// Damped current value /* Damped current value */
if (pMeasurement->fDampedValue == INITIALISATION_VALUE) if (pMeasurement->fDampedValue == INITIALISATION_VALUE)
{ {
pMeasurement->fDampedValue = pMeasurement->fCurrentValue; 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) if (!pMeasurement)
return; return;
@ -205,7 +237,11 @@ void updatePrediction(sMeasurement *pMeasurement)
predict60s->bufferCount + 60.0f); 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) 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) 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; float sumX = INITIALISATION_VALUE, sumY = INITIALISATION_VALUE, sumXY = INITIALISATION_VALUE, sumX2 = INITIALISATION_VALUE;
for (size_t i = 0; i < count; i++) for (size_t i = 0; i < count; i++)
{ {
// Calculate the circular buffer index for the current sample
size_t circularIndex = (bufferIndex + i + 1) % count; size_t circularIndex = (bufferIndex + i + 1) % count;
float x = (float)i; // Time index float x = (float)i;
float y = samples[circularIndex]; // Sample value float y = samples[circularIndex];
sumX += x; sumX += x;
sumY += y; sumY += y;
@ -328,15 +371,13 @@ float linearRegressionPredict(const float *samples, size_t count, size_t bufferI
sumX2 += x * x; sumX2 += x * x;
} }
// Calculate slope (m) and intercept (b) of the line: y = mx + b
float denominator = (count * sumX2 - sumX * sumX); float denominator = (count * sumX2 - sumX * sumX);
if (fabs(denominator) < 1e-6) // Avoid division by zero if (fabs(denominator) < 1e-6)
return samples[bufferIndex]; // Return the latest value as prediction return samples[bufferIndex];
float m = (count * sumXY - sumX * sumY) / denominator; float m = (count * sumXY - sumX * sumY) / denominator;
float b = (sumY - m * sumX) / count; float b = (sumY - m * sumX) / count;
// Predict value at futureIndex
return m * futureIndex + b; return m * futureIndex + b;
} }

View File

@ -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 #pragma once
#include "sdkconfig.h" #include "sdkconfig.h"
#include "esp_err.h"
#include <stddef.h> #include <stddef.h>
/** @brief Returns the maximum of two values. */
#define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MAX(a, b) ((a) > (b) ? (a) : (b))
/** @brief Initial value for measurements before first reading. */
#define INITIALISATION_VALUE 0.0f #define INITIALISATION_VALUE 0.0f
/** @brief Sample buffer size for 10-second average. */
#define AVG10S_SAMPLE_SIZE 10U #define AVG10S_SAMPLE_SIZE 10U
/** @brief Sample buffer size for 60-second average. */
#define AVG60S_SAMPLE_SIZE 60U #define AVG60S_SAMPLE_SIZE 60U
/** @brief Sample buffer size for 24-hour average. */
#define AVG24H_SAMPLE_SIZE 24U #define AVG24H_SAMPLE_SIZE 24U
/** @brief Sample buffer size for 60-second prediction. */
#define PRED60S_SAMPLE_SIZE 60U #define PRED60S_SAMPLE_SIZE 60U
/** @brief Damping factor for rising temperatures (from Kconfig). */
#define DAMPING_FACTOR_WARMER (CONFIG_DAMPING_FACTOR_WARMER * 0.00001f) #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) #define DAMPING_FACTOR_COLDER (CONFIG_DAMPING_FACTOR_COLDER * 0.00001f)
/**
* @brief Burner error state enumeration.
*/
typedef enum _BurnerErrorState typedef enum _BurnerErrorState
{ {
NO_ERROR, NO_ERROR, /**< No burner fault detected. */
FAULT FAULT /**< Burner fault signal active. */
} eBurnerErrorState; } eBurnerErrorState;
/**
* @brief Measurement error state enumeration.
*/
typedef enum _MeasurementErrorState typedef enum _MeasurementErrorState
{ {
MEASUREMENT_NO_ERROR, MEASUREMENT_NO_ERROR, /**< Measurement valid. */
MEASUREMENT_FAULT MEASUREMENT_FAULT /**< Measurement failed or sensor not found. */
} eMeasurementErrorState; } eMeasurementErrorState;
/**
* @brief Circular buffer for averaging temperature values.
*/
typedef struct _Average typedef struct _Average
{ {
float fValue; float fValue; /**< Current average value. */
float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))]; float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))]; /**< Sample buffer. */
size_t bufferIndex; size_t bufferIndex; /**< Current write index. */
size_t bufferCount; size_t bufferCount; /**< Number of valid samples. */
} sAverage; } sAverage;
/**
* @brief Circular buffer for temperature prediction.
*/
typedef struct _Predict typedef struct _Predict
{ {
float fValue; float fValue; /**< Predicted value. */
float samples[PRED60S_SAMPLE_SIZE]; float samples[PRED60S_SAMPLE_SIZE]; /**< Sample buffer. */
size_t bufferIndex; size_t bufferIndex; /**< Current write index. */
size_t bufferCount; size_t bufferCount; /**< Number of valid samples. */
} sPredict; } sPredict;
/**
* @brief Complete measurement data structure.
*/
typedef struct _Measurement typedef struct _Measurement
{ {
float fCurrentValue; float fCurrentValue; /**< Current raw temperature value. */
float fDampedValue; float fDampedValue; /**< Damped temperature value. */
sAverage average10s; sAverage average10s; /**< 10-second rolling average. */
sAverage average60s; sAverage average60s; /**< 60-second rolling average. */
sPredict predict60s; sPredict predict60s; /**< 60-second prediction. */
eMeasurementErrorState state; eMeasurementErrorState state; /**< Measurement state. */
} sMeasurement; } 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); sMeasurement getChamberTemperature(void);
/**
* @brief Get the current outdoor temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getOutdoorTemperature(void); sMeasurement getOutdoorTemperature(void);
/**
* @brief Get the current inlet flow temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getInletFlowTemperature(void); sMeasurement getInletFlowTemperature(void);
/**
* @brief Get the current return flow temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getReturnFlowTemperature(void); sMeasurement getReturnFlowTemperature(void);
/**
* @brief Get the current burner error state.
* @return eBurnerErrorState indicating fault status.
*/
eBurnerErrorState getBurnerError(void); eBurnerErrorState getBurnerError(void);

View File

@ -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 "safety.h"
#include "metrics.h" #include "metrics.h"
#include "outputs.h" #include "outputs.h"
@ -9,30 +17,107 @@
#include "esp_log.h" #include "esp_log.h"
#include "esp_system.h" #include "esp_system.h"
#include "nvs_flash.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) 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(); esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) 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()); ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init(); 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! /* Initialize Outputs */
initOutputs(); if (initOutputs() != ESP_OK)
initInputs(); {
initSafety(); reboot_on_error("Outputs");
initWifi(); }
initSntp(); ESP_LOGI(TAG, "[OK] Outputs initialized");
initControl();
initMetrics(); /* 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) while (1)
{ {

View File

@ -1,3 +1,8 @@
/**
* @file metrics.c
* @brief Implementation of Prometheus metrics endpoint.
*/
#include "metrics.h" #include "metrics.h"
#include "outputs.h" #include "outputs.h"
#include "inputs.h" #include "inputs.h"
@ -15,41 +20,50 @@
#include <time.h> #include <time.h>
#include <sys/time.h> #include <sys/time.h>
static const char *TAG = "smart-oil-heater-control-system-metrics"; static const char *TAG = "metrics";
char caHtmlResponse[HTML_RESPONSE_SIZE]; static char caHtmlResponse[HTML_RESPONSE_SIZE];
SemaphoreHandle_t xMutexAccessMetricResponse = NULL; static SemaphoreHandle_t xMutexAccessMetricResponse = NULL;
static sMetric aMetrics[METRIC_MAX_COUNT]; static sMetric aMetrics[METRIC_MAX_COUNT];
static uint16_t u16MetricCounter = 0U; static uint16_t u16MetricCounter = 0U;
void taskMetrics(void *pvParameters); /* Private function prototypes */
httpd_handle_t setup_server(void); static void taskMetrics(void *pvParameters);
esp_err_t get_metrics_handler(httpd_req_t *req); 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( BaseType_t taskCreated = xTaskCreate(
taskMetrics, // Function to implement the task taskMetrics,
"taskMetrics", // Task name "taskMetrics",
32768, // Stack size (in words, not bytes) 32768,
NULL, // Parameters to the task function (none in this case) NULL,
5, // Task priority (higher number = higher priority) 5,
NULL // Task handle (optional) NULL);
);
if (taskCreated == pdPASS) if (taskCreated != pdPASS)
{
ESP_LOGI(TAG, "Task created successfully!");
}
else
{ {
ESP_LOGE(TAG, "Failed to create task"); 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) while (1)
{ {
@ -338,8 +352,6 @@ void vSetMetrics(sMetric *paMetrics, uint16_t u16Size)
break; break;
} }
// printf("%s\n", paMetrics[u16Index].caMetricName);
// printf("%s\n", caValueBuffer);
strcat(caHtmlResponse, paMetrics[u16Index].caMetricName); strcat(caHtmlResponse, paMetrics[u16Index].caMetricName);
strcat(caHtmlResponse, caValueBuffer); strcat(caHtmlResponse, caValueBuffer);
strcat(caHtmlResponse, "\n"); 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) 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(); httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 9100; config.server_port = 9100;
@ -382,14 +403,17 @@ httpd_handle_t setup_server(void)
xMutexAccessMetricResponse = xSemaphoreCreateRecursiveMutex(); xMutexAccessMetricResponse = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessMetricResponse == NULL) if (xMutexAccessMetricResponse == NULL)
{ {
ESP_LOGE(TAG, "Unable to create mutex for metric response"); ESP_LOGE(TAG, "Failed to create mutex");
return NULL;
} }
xSemaphoreGiveRecursive(xMutexAccessMetricResponse); xSemaphoreGiveRecursive(xMutexAccessMetricResponse);
if (httpd_start(&server, &config) == ESP_OK) if (httpd_start(&server, &config) == ESP_OK)
{ {
httpd_register_uri_handler(server, &uri_get); httpd_register_uri_handler(server, &uri_get);
return server;
} }
return server; ESP_LOGE(TAG, "Failed to start HTTP server");
return NULL;
} }

View File

@ -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 #pragma once
#include <esp_http_server.h> #include "esp_err.h"
#include "esp_http_server.h"
#include <stdint.h>
/** @brief Maximum size of HTTP response buffer. */
#define HTML_RESPONSE_SIZE 4096U #define HTML_RESPONSE_SIZE 4096U
/** @brief Maximum length of metric name. */
#define METRIC_NAME_MAX_SIZE 64U #define METRIC_NAME_MAX_SIZE 64U
/** @brief Maximum number of metrics. */
#define METRIC_MAX_COUNT 38U #define METRIC_MAX_COUNT 38U
/**
* @brief Metric value type enumeration.
*/
typedef enum _MetricValueType typedef enum _MetricValueType
{ {
FLOAT, FLOAT, /**< Floating point value. */
INTEGER_U8, INTEGER_U8, /**< 8-bit unsigned integer. */
INTEGER_64, INTEGER_64, /**< 64-bit signed integer. */
} eMetricValueType; } eMetricValueType;
/**
* @brief Metric data structure.
*/
typedef struct _metric typedef struct _metric
{ {
char caMetricName[METRIC_NAME_MAX_SIZE]; char caMetricName[METRIC_NAME_MAX_SIZE]; /**< Metric name. */
eMetricValueType type; eMetricValueType type; /**< Value type. */
float fMetricValue; float fMetricValue; /**< Float value (if type is FLOAT). */
uint8_t u8MetricValue; uint8_t u8MetricValue; /**< U8 value (if type is INTEGER_U8). */
int64_t i64MetricValue; int64_t i64MetricValue; /**< I64 value (if type is INTEGER_64). */
} sMetric; } sMetric;
void initMetrics(void); /**
void vSetMetrics(sMetric *paMetrics, uint16_t u16Size); * @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);

View File

@ -1,3 +1,8 @@
/**
* @file outputs.c
* @brief Implementation of output control module.
*/
#include "outputs.h" #include "outputs.h"
#include "sdkconfig.h" #include "sdkconfig.h"
@ -6,69 +11,76 @@
#include "driver/gpio.h" #include "driver/gpio.h"
#include "esp_log.h" #include "esp_log.h"
static const char *TAG = "smart-oil-heater-control-system-outputs"; static const char *TAG = "outputs";
const uint8_t uCirculationPumpGpioPin = CONFIG_GPIO_CIRCULATION_PUMP;
const uint8_t uBurnerGpioPin = CONFIG_GPIO_BURNER; /** @brief Circulation pump GPIO pin (from Kconfig). */
const uint8_t uSafetyContactGpioPin = CONFIG_GPIO_SAFETY_CONTACT; 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 SemaphoreHandle_t xMutexAccessOutputs = NULL;
static eOutput sCirculationPumpState; static eOutput sCirculationPumpState;
static eOutput sBurnerState; static eOutput sBurnerState;
static eOutput sSafetyContactState; static eOutput sSafetyContactState;
void initOutputs(void) esp_err_t initOutputs(void)
{ {
gpio_config_t ioConfCirculationPump = { gpio_config_t ioConfCirculationPump = {
.pin_bit_mask = (1ULL << uCirculationPumpGpioPin), // Pin mask .pin_bit_mask = (1ULL << uCirculationPumpGpioPin),
.mode = GPIO_MODE_OUTPUT, // Set as output .mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up .pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down .pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE // Disable interrupts .intr_type = GPIO_INTR_DISABLE};
};
gpio_config_t ioConfBurner = { gpio_config_t ioConfBurner = {
.pin_bit_mask = (1ULL << uBurnerGpioPin), // Pin mask .pin_bit_mask = (1ULL << uBurnerGpioPin),
.mode = GPIO_MODE_OUTPUT, // Set as output .mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up .pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down .pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE // Disable interrupts .intr_type = GPIO_INTR_DISABLE};
};
gpio_config_t ioConfSafetyContact = { gpio_config_t ioConfSafetyContact = {
.pin_bit_mask = (1ULL << uSafetyContactGpioPin), // Pin mask .pin_bit_mask = (1ULL << uSafetyContactGpioPin),
.mode = GPIO_MODE_OUTPUT, // Set as output .mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up .pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down .pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE // Disable interrupts .intr_type = GPIO_INTR_DISABLE};
};
esp_err_t ret = gpio_config(&ioConfCirculationPump); esp_err_t ret = gpio_config(&ioConfCirculationPump);
if (ret != ESP_OK) if (ret != ESP_OK)
{ {
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); ESP_LOGE(TAG, "GPIO config failed for circulation pump: %s", esp_err_to_name(ret));
return; return ESP_FAIL;
} }
ret = gpio_config(&ioConfBurner); ret = gpio_config(&ioConfBurner);
if (ret != ESP_OK) if (ret != ESP_OK)
{ {
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); ESP_LOGE(TAG, "GPIO config failed for burner: %s", esp_err_to_name(ret));
return; return ESP_FAIL;
} }
ret = gpio_config(&ioConfSafetyContact); ret = gpio_config(&ioConfSafetyContact);
if (ret != ESP_OK) if (ret != ESP_OK)
{ {
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); ESP_LOGE(TAG, "GPIO config failed for safety contact: %s", esp_err_to_name(ret));
return; return ESP_FAIL;
} }
xMutexAccessOutputs = xSemaphoreCreateRecursiveMutex(); xMutexAccessOutputs = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessOutputs == NULL) if (xMutexAccessOutputs == NULL)
{ {
ESP_LOGE(TAG, "Unable to create mutex"); ESP_LOGE(TAG, "Failed to create mutex");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessOutputs); xSemaphoreGiveRecursive(xMutexAccessOutputs);
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
} }
eOutput getCirculationPumpState(void) eOutput getCirculationPumpState(void)

View File

@ -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 #pragma once
#include "esp_err.h"
/**
* @brief Output state enumeration.
*/
typedef enum _Output typedef enum _Output
{ {
ENABLED, ENABLED, /**< Output active (relay energized, GPIO low). */
DISABLED DISABLED /**< Output inactive (relay de-energized, GPIO high). */
} eOutput; } 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); eOutput getCirculationPumpState(void);
/**
* @brief Set the circulation pump state.
* @param in Desired state (ENABLED or DISABLED).
*/
void setCirculationPumpState(eOutput in); void setCirculationPumpState(eOutput in);
/**
* @brief Get the current burner state.
* @return eOutput state (ENABLED or DISABLED).
*/
eOutput getBurnerState(void); eOutput getBurnerState(void);
/**
* @brief Set the burner state.
* @param in Desired state (ENABLED or DISABLED).
*/
void setBurnerState(eOutput in); void setBurnerState(eOutput in);
/**
* @brief Get the current safety contact state.
* @return eOutput state (ENABLED or DISABLED).
*/
eOutput getSafetyControlState(void); eOutput getSafetyControlState(void);
void setSafetyControlState(eOutput in);
/**
* @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);

View File

@ -1,3 +1,8 @@
/**
* @file safety.c
* @brief Implementation of safety monitoring module.
*/
#include "safety.h" #include "safety.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
@ -7,53 +12,68 @@
#include <string.h> #include <string.h>
#include <math.h> #include <math.h>
#define PERIODIC_INTERVAL 1U // run safety checks every 1sec /** @brief Task interval in seconds. */
#define SENSOR_GRACE_PERIOD (CONFIG_SENSOR_GRACE_PERIOD_MINUTES * 60U) // period that a sensor can report the same reading 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 #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; static SemaphoreHandle_t xMutexAccessSafety = NULL;
/** @brief Sensor sanity check configurations. */
static sSensorSanityCheck sanityChecks[NUMBER_OF_SENSOR_SANITY_CHECKS] = { 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, "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, "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, "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}}; {SENSOR_NO_ERROR, "return_flow_temperature", {SENSOR_LIMIT_RETURN_MAX, SENSOR_LIMIT_RETURN_MIN}, 0.0f, 0U, getReturnFlowTemperature}};
static eSafetyState sSafetyState = SAFETY_NO_ERROR; static eSafetyState sSafetyState = SAFETY_NO_ERROR;
void taskSafety(void *pvParameters); /* Private function prototypes */
void checkSensorSanity(void); static void taskSafety(void *pvParameters);
void setSafeState(void); static void checkSensorSanity(void);
static void setSafeState(void);
void initSafety(void) esp_err_t initSafety(void)
{ {
xMutexAccessSafety = xSemaphoreCreateRecursiveMutex(); xMutexAccessSafety = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessSafety == NULL) if (xMutexAccessSafety == NULL)
{ {
ESP_LOGE(TAG, "Unable to create mutex"); ESP_LOGE(TAG, "Failed to create mutex");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessSafety); xSemaphoreGiveRecursive(xMutexAccessSafety);
BaseType_t taskCreated = xTaskCreate( BaseType_t taskCreated = xTaskCreate(
taskSafety, // Function to implement the task taskSafety,
"taskSafety", // Task name "taskSafety",
4096, // Stack size (in words, not bytes) 4096,
NULL, // Parameters to the task function (none in this case) NULL,
5, // Task priority (higher number = higher priority) 5,
NULL // Task handle (optional) NULL);
);
if (taskCreated == pdPASS) if (taskCreated != pdPASS)
{
ESP_LOGI(TAG, "Task created successfully!");
}
else
{ {
ESP_LOGE(TAG, "Failed to create task"); 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) while (1)
{ {
@ -61,7 +81,6 @@ void taskSafety(void *pvParameters)
if (xSemaphoreTakeRecursive(xMutexAccessSafety, portMAX_DELAY) == pdTRUE) if (xSemaphoreTakeRecursive(xMutexAccessSafety, portMAX_DELAY) == pdTRUE)
{ {
checkSensorSanity(); checkSensorSanity();
if (sSafetyState != SAFETY_NO_ERROR) 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; sSafetyState = SAFETY_NO_ERROR;
for (int i = 0; i < NUMBER_OF_SENSOR_SANITY_CHECKS; i++) 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 setCirculationPumpState(ENABLED); // To cool down system
setBurnerState(DISABLED); // Deactivate burner setBurnerState(DISABLED); // Deactivate burner

View File

@ -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 #pragma once
#include "outputs.h" #include "outputs.h"
#include "inputs.h" #include "inputs.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#include "esp_err.h"
#include <stdint.h> #include <stdint.h>
/** @brief Maximum length of sensor name string. */
#define MAX_ERROR_STRING_SIZE 64U #define MAX_ERROR_STRING_SIZE 64U
/** @brief Number of sensors to monitor. */
#define NUMBER_OF_SENSOR_SANITY_CHECKS 4U #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) #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) #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) #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) #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) #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) #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) #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) #define SENSOR_LIMIT_RETURN_MIN (CONFIG_SENSOR_LIMIT_RETURN_MIN / 10.0f)
/**
* @brief Sensor error state enumeration.
*/
typedef enum _SensorErrorState typedef enum _SensorErrorState
{ {
SENSOR_NO_ERROR, SENSOR_NO_ERROR, /**< Sensor operating normally. */
SENSOR_TOO_HIGH, SENSOR_TOO_HIGH, /**< Temperature above maximum limit. */
SENSOR_TOO_LOW, SENSOR_TOO_LOW, /**< Temperature below minimum limit. */
SENSOR_UNCHANGED, SENSOR_UNCHANGED, /**< Temperature unchanged for too long. */
SENSOR_NOT_FOUND SENSOR_NOT_FOUND /**< Sensor not responding. */
} eSensorErrorState; } eSensorErrorState;
/**
* @brief Overall safety state enumeration.
*/
typedef enum _SafetyState typedef enum _SafetyState
{ {
SAFETY_NO_ERROR, SAFETY_NO_ERROR, /**< All sensors OK. */
SAFETY_SENSOR_ERROR, SAFETY_SENSOR_ERROR, /**< At least one sensor failed. */
SAFETY_INTERNAL_ERROR SAFETY_INTERNAL_ERROR /**< Internal module error. */
} eSafetyState; } eSafetyState;
/**
* @brief Function pointer type for sensor getter functions.
*/
typedef sMeasurement (*GetSensorValue)(); typedef sMeasurement (*GetSensorValue)();
/**
* @brief Temperature sensor limits.
*/
typedef struct _TemperatureSensorLimit typedef struct _TemperatureSensorLimit
{ {
float max; // Maximum temperature limit float max; /**< Maximum temperature limit. */
float min; // Minimum temperature limit float min; /**< Minimum temperature limit. */
} sTemperatureSensorLimit; } sTemperatureSensorLimit;
/**
* @brief Sensor sanity check state structure.
*/
typedef struct _SensorSanityCheck typedef struct _SensorSanityCheck
{ {
eSensorErrorState state; eSensorErrorState state; /**< Current error state. */
char name[MAX_ERROR_STRING_SIZE]; char name[MAX_ERROR_STRING_SIZE]; /**< Sensor name for logging. */
sTemperatureSensorLimit sSensorLimit; sTemperatureSensorLimit sSensorLimit; /**< Temperature limits. */
float fSensorTemperatureLast; float fSensorTemperatureLast; /**< Last temperature reading. */
uint32_t uUnchangedCounter; uint32_t uUnchangedCounter; /**< Counter for unchanged readings. */
GetSensorValue getSensor; GetSensorValue getSensor; /**< Function to get sensor value. */
} sSensorSanityCheck; } 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); void getSensorSanityStates(sSensorSanityCheck *pSensorSanityChecks);
eSafetyState getSafetyState(void);
/**
* @brief Get the overall safety state.
* @return eSafetyState indicating current safety status.
*/
eSafetyState getSafetyState(void);

View File

@ -1,3 +1,8 @@
/**
* @file sntp.c
* @brief Implementation of SNTP client module.
*/
#include "sntp.h" #include "sntp.h"
#include "esp_sntp.h" #include "esp_sntp.h"
@ -6,17 +11,22 @@
#include <time.h> #include <time.h>
#include <sys/time.h> #include <sys/time.h>
static const char *TAG = "smart-oil-heater-control-system-sntp"; static const char *TAG = "sntp";
static volatile eSntpState sntpState = SYNC_NOT_STARTED;
void time_sync_notification_cb(struct timeval *tv);
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_setoperatingmode(SNTP_OPMODE_POLL);
esp_sntp_setservername(0, CONFIG_SNTP_SERVER_IP_ADDR); esp_sntp_setservername(0, CONFIG_SNTP_SERVER_IP_ADDR);
sntp_set_time_sync_notification_cb(time_sync_notification_cb); sntp_set_time_sync_notification_cb(time_sync_notification_cb);
esp_sntp_init(); esp_sntp_init();
ESP_LOGI(TAG, "Initialized successfully, server: %s", CONFIG_SNTP_SERVER_IP_ADDR);
return ESP_OK;
} }
eSntpState getSntpState(void) eSntpState getSntpState(void)
@ -24,8 +34,12 @@ eSntpState getSntpState(void)
return sntpState; 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; sntpState = SYNC_SUCCESSFUL;
} }

View File

@ -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 #pragma once
#include "esp_err.h"
/**
* @brief SNTP synchronization state enumeration.
*/
typedef enum _SntpState typedef enum _SntpState
{ {
SYNC_SUCCESSFUL, SYNC_SUCCESSFUL, /**< Time synchronized successfully. */
SYNC_NOT_STARTED, SYNC_NOT_STARTED, /**< Synchronization not yet attempted. */
SYNC_FAILED, SYNC_FAILED, /**< Synchronization failed. */
} eSntpState; } eSntpState;
void initSntp(void); /**
eSntpState getSntpState(void); * @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);

View File

@ -1,3 +1,8 @@
/**
* @file wifi.c
* @brief Implementation of WiFi station mode module.
*/
#include "wifi.h" #include "wifi.h"
#include "esp_timer.h" #include "esp_timer.h"
@ -12,12 +17,19 @@
#include <string.h> #include <string.h>
/** @brief Event bit for successful connection. */
#define WIFI_CONNECTED_BIT BIT0 #define WIFI_CONNECTED_BIT BIT0
/** @brief Event bit for connection failure. */
#define WIFI_FAIL_BIT BIT1 #define WIFI_FAIL_BIT BIT1
/** @brief Maximum connection retry attempts. */
#define MAX_RETRY_COUNT 10 #define MAX_RETRY_COUNT 10
/** @brief Delay between retries in milliseconds. */
#define RETRY_DELAY_MS 1000 #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 EventGroupHandle_t s_wifi_event_group;
static int s_retry_num = 0; 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, static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data); int32_t event_id, void *event_data);
void initWifi(void) esp_err_t initWifi(void)
{ {
s_wifi_event_group = xEventGroupCreate(); s_wifi_event_group = xEventGroupCreate();
if (s_wifi_event_group == NULL) if (s_wifi_event_group == NULL)
{ {
ESP_LOGE(TAG, "xEventGroupCreate() failed!"); ESP_LOGE(TAG, "Failed to create event group");
return; return ESP_FAIL;
} }
ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_netif_init());
@ -73,10 +85,10 @@ void initWifi(void)
ESP_ERROR_CHECK(esp_wifi_start()); 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_max_tx_power(78));
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); // Use power-saving mode 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, EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
@ -86,21 +98,32 @@ void initWifi(void)
if (bits & WIFI_CONNECTED_BIT) 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) 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 else
{ {
ESP_LOGE(TAG, "Unexpected event"); ESP_LOGE(TAG, "Unexpected event");
return ESP_FAIL;
} }
// Mark initial connection phase complete - do NOT delete the event group
s_initial_connect = false; 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, static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) int32_t event_id, void *event_data)
{ {

View File

@ -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 #pragma once
void initWifi(void); #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);