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
### Design
ESP32-based control system for oil-fired central heating with schedule-based temperature management, safety monitoring, and Prometheus metrics export.
## 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
classDiagram
Inputs <|-- Control
Outputs <|-- Control
Sntp <|-- Control
Inputs <|-- Safety
Outputs <|--|> Safety
flowchart TB
subgraph OUTSIDE[" "]
OT[/"🌡️ Outdoor Temp<br/>DS18B20"/]
end
Inputs <|-- Metrics
Outputs <|-- Metrics
Control <|-- Metrics
Safety <|-- Metrics
Sntp <|-- Metrics
subgraph BURNER["OIL BURNER"]
CT[/"🌡️ Chamber Temp<br/>DS18B20"/]
BF[["⚠️ Burner Fault<br/>GPIO19 INPUT"]]
BR(["🔥 Burner Relay<br/>GPIO14"])
SC(["🔌 Safety Contact<br/>GPIO12"])
end
class Inputs{
+initInputs()
-initMeasurement()
-updateAverage()
-updatePrediction()
-taskInput()
-linearRegressionPredict()
+getChamberTemperature()
+getOutdoorTemperature()
+getInletFlowTemperature()
+getReturnFlowTemperature()
+getBurnerError()
}
subgraph CIRCUIT["HEATING CIRCUIT"]
IT[/"🌡️ Inlet Temp<br/>DS18B20"/]
CP(["💧 Circulation Pump<br/>GPIO27"])
RT[/"🌡️ Return Temp<br/>DS18B20"/]
end
class Outputs{
+initOutputs()
+getCirculationPumpState()
+setCirculationPumpState()
+getBurnerState()
+setBurnerState()
+getSafetyControlState()
+setSafetyControlState()
}
RAD["🏠 Radiators"]
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()
}
BURNER -->|"hot water"| IT
IT --> CP
CP --> RAD
RAD --> RT
RT -->|"cold water"| BURNER
```
### Prometheus Metrics
@ -88,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
@ -115,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
@ -180,4 +138,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
```

View File

@ -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 <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;
// 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;
}
void taskControl(void *pvParameters)
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
}
/**
* @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,6 +357,7 @@ 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",

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
#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
/** @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);

View File

@ -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 <string.h>
#include <math.h>
/** @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;
}
void initMeasurement(sMeasurement *pMeasurement)
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
}
/**
* @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;
}

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
#include "sdkconfig.h"
#include "esp_err.h"
#include <stddef.h>
/** @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);

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 "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)
{

View File

@ -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 <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];
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;
}
void taskMetrics(void *pvParameters)
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
}
/**
* @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;
}
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
#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
/** @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);
/**
* @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 "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)

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
#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);
/**
* @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 "freertos/FreeRTOS.h"
@ -7,53 +12,68 @@
#include <string.h>
#include <math.h>
#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

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
#include "outputs.h"
#include "inputs.h"
#include "sdkconfig.h"
#include "esp_err.h"
#include <stdint.h>
/** @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);
/**
* @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 "esp_sntp.h"
@ -6,17 +11,22 @@
#include <time.h>
#include <sys/time.h>
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;
}

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
#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);
/**
* @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 "esp_timer.h"
@ -12,12 +17,19 @@
#include <string.h>
/** @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)
{

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
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);