1 Commits

Author SHA256 Message Date
618a6974bf enable ap lock 2026-01-09 23:26:46 +01:00
17 changed files with 618 additions and 1642 deletions

168
README.md
View File

@ -1,41 +1,83 @@
# Smart Oil Heating Control System # smart-oil-heating-control-system
ESP32-based control system for oil-fired central heating with schedule-based temperature management, safety monitoring, and Prometheus metrics export. ## Software
### 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
flowchart TB classDiagram
subgraph OUTSIDE[" "] Inputs <|-- Control
OT[/"🌡️ Outdoor Temp<br/>DS18B20"/] Outputs <|-- Control
end Sntp <|-- Control
Inputs <|-- Safety
Outputs <|--|> Safety
subgraph BURNER["OIL BURNER"] Inputs <|-- Metrics
CT[/"🌡️ Chamber Temp<br/>DS18B20"/] Outputs <|-- Metrics
BF[["⚠️ Burner Fault<br/>GPIO19 INPUT"]] Control <|-- Metrics
BR(["🔥 Burner Relay<br/>GPIO14"]) Safety <|-- Metrics
SC(["🔌 Safety Contact<br/>GPIO12"]) Sntp <|-- Metrics
end
subgraph CIRCUIT["HEATING CIRCUIT"] class Inputs{
IT[/"🌡️ Inlet Temp<br/>DS18B20"/] +initInputs()
CP(["💧 Circulation Pump<br/>GPIO27"]) -initMeasurement()
RT[/"🌡️ Return Temp<br/>DS18B20"/] -updateAverage()
end -updatePrediction()
-taskInput()
-linearRegressionPredict()
+getChamberTemperature()
+getOutdoorTemperature()
+getInletFlowTemperature()
+getReturnFlowTemperature()
+getBurnerError()
}
RAD["🏠 Radiators"] class Outputs{
+initOutputs()
+getCirculationPumpState()
+setCirculationPumpState()
+getBurnerState()
+setBurnerState()
+getSafetyControlState()
+setSafetyControlState()
}
BURNER -->|"hot water"| IT class Control{
IT --> CP initControl()
CP --> RAD +taskControl()
RAD --> RT +getControlCurrentWeekday()
RT -->|"cold water"| BURNER -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()
}
``` ```
### Prometheus Metrics ### Prometheus Metrics
@ -46,26 +88,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.312500 chamber_temperature 37.250000
chamber_temperature_avg10 37.393749 chamber_temperature_avg10 37.237499
chamber_temperature_avg60 37.689583 chamber_temperature_avg60 37.438541
chamber_temperature_damped 38.058098 chamber_temperature_damped 42.185040
chamber_temperature_pred60 36.697266 chamber_temperature_pred60 36.638443
inlet_flow_temperature 34.562500 inlet_flow_temperature 35.625000
inlet_flow_temperature_avg10 34.587502 inlet_flow_temperature_avg10 35.618752
inlet_flow_temperature_avg60 34.880207 inlet_flow_temperature_avg60 35.415627
inlet_flow_temperature_damped 35.255993 inlet_flow_temperature_damped 39.431259
inlet_flow_temperature_pred60 33.910374 inlet_flow_temperature_pred60 36.078678
outdoor_temperature 1.812500 outdoor_temperature 14.687500
outdoor_temperature_avg10 1.825000 outdoor_temperature_avg10 14.662500
outdoor_temperature_avg60 1.821875 outdoor_temperature_avg60 14.646875
outdoor_temperature_damped 2.390663 outdoor_temperature_damped 9.169084
outdoor_temperature_pred60 1.840263 outdoor_temperature_pred60 14.660233
return_flow_temperature 34.125000 return_flow_temperature 39.937500
return_flow_temperature_avg10 34.162498 return_flow_temperature_avg10 40.087502
return_flow_temperature_avg60 34.304165 return_flow_temperature_avg60 41.146873
return_flow_temperature_damped 31.430506 return_flow_temperature_damped 32.385151
return_flow_temperature_pred60 33.858772 return_flow_temperature_pred60 37.311958
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
@ -73,13 +115,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 24300 control_current_entry_time 17100
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 1768067412 system_unixtime 1762012743
uptime_seconds 344878 uptime_seconds 465229
wifi_rssi -59 wifi_rssi -72
``` ```
#### Status Encoding #### Status Encoding
@ -138,20 +180,4 @@ wifi_rssi -59
| 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,391 +1,36 @@
menu "Smart Oil Heating Control System" menu "Smart Oil Heating Control System"
menu "WiFi Configuration" config SSID
config SSID string "SSID"
string "WiFi SSID" default "my WiFi SSID"
default "my WiFi SSID" config WIFI_PASSWORD
help string "WIFI_PASSWORD"
The SSID of the WiFi network to connect to. default "my WIFI Password"
config STATIC_IP_ADDR
config WIFI_PASSWORD string "Static IPv4 address"
string "WiFi Password" default "192.168.0.42"
default "my WIFI Password" config STATIC_IP_NETMASK
help string "Static IPv4 netmask"
The password for the WiFi network. default "255.255.0.0"
config STATIC_GATEWAY_IP_ADDR
config STATIC_IP_ADDR string "Static IPv4 gateway address"
string "Static IPv4 address" default "192.168.0.1"
default "192.168.0.42" config SNTP_SERVER_IP_ADDR
help string "SNTP IPv4 server address"
Static IP address for the ESP32. default "192.168.0.1"
config ENV_WIFI_BSSID_LOCK
config STATIC_IP_NETMASK bool "Lock to specific Access Point (BSSID)"
string "Static IPv4 netmask" default n
default "255.255.0.0" help
help When enabled, the device will only connect to the access point
Network mask for the static IP configuration. with the specified MAC address (BSSID). Useful when multiple APs
share the same SSID.
config STATIC_GATEWAY_IP_ADDR config ENV_WIFI_BSSID
string "Static IPv4 gateway address" string "Access Point MAC Address (BSSID)"
default "192.168.0.1" default "00:00:00:00:00:00"
help depends on ENV_WIFI_BSSID_LOCK
Gateway IP address for network routing. help
MAC address of the access point to connect to.
config SNTP_SERVER_IP_ADDR Format: XX:XX:XX:XX:XX:XX (uppercase or lowercase)
string "SNTP server address"
default "192.168.0.1"
help
NTP server address for time synchronization.
endmenu
menu "GPIO Configuration"
menu "Input GPIOs"
config GPIO_BURNER_FAULT
int "Burner fault input GPIO"
range 0 39
default 19
help
GPIO pin connected to the burner fault signal.
config GPIO_DS18B20_ONEWIRE
int "DS18B20 1-Wire bus GPIO"
range 0 39
default 4
help
GPIO pin for the 1-Wire bus (DS18B20 temperature sensors).
endmenu
menu "Output GPIOs"
config GPIO_CIRCULATION_PUMP
int "Circulation pump output GPIO"
range 0 39
default 27
help
GPIO pin to control the circulation pump relay.
config GPIO_BURNER
int "Burner control output GPIO"
range 0 39
default 14
help
GPIO pin to control the burner relay.
config GPIO_SAFETY_CONTACT
int "Safety contact output GPIO"
range 0 39
default 12
help
GPIO pin for the safety contact relay (main power to burner).
endmenu
endmenu
menu "1-Wire Sensor Addresses"
config ONEWIRE_ADDR_CHAMBER_TEMP
hex "Chamber temperature sensor address"
default 0xd00000108cd01d28
help
64-bit 1-Wire address of the chamber temperature sensor.
config ONEWIRE_ADDR_OUTDOOR_TEMP
hex "Outdoor temperature sensor address"
default 0xd70000108a9b9128
help
64-bit 1-Wire address of the outdoor temperature sensor.
config ONEWIRE_ADDR_INLET_FLOW_TEMP
hex "Inlet flow temperature sensor address"
default 0x410000108b8c0628
help
64-bit 1-Wire address of the inlet flow temperature sensor.
config ONEWIRE_ADDR_RETURN_FLOW_TEMP
hex "Return flow temperature sensor address"
default 0x90000108cc77c28
help
64-bit 1-Wire address of the return flow temperature sensor.
endmenu
menu "Temperature Control Settings"
menu "Target Temperatures"
config TEMP_RETURN_FLOW_LOWER_LIMIT_DAY
int "Return flow lower limit (day) [°C x 10]"
range 150 500
default 300
help
Minimum return flow temperature during day mode in 0.1°C units.
Example: 300 = 30.0°C
config TEMP_RETURN_FLOW_LOWER_LIMIT_NIGHT
int "Return flow lower limit (night) [°C x 10]"
range 150 500
default 250
help
Minimum return flow temperature during night mode in 0.1°C units.
Example: 250 = 25.0°C
config TEMP_CHAMBER_TARGET
int "Chamber target temperature [°C x 10]"
range 500 950
default 800
help
Maximum chamber temperature target in 0.1°C units.
Example: 800 = 80.0°C
config TEMP_CHAMBER_THRESHOLD
int "Chamber temperature threshold [°C x 10]"
range 300 700
default 450
help
Minimum chamber temperature to enable burner in 0.1°C units.
Example: 450 = 45.0°C
config TEMP_CIRCULATION_PUMP_THRESHOLD
int "Circulation pump threshold [°C x 10]"
range 200 500
default 300
help
Minimum chamber temperature to enable circulation pump in 0.1°C units.
Example: 300 = 30.0°C
endmenu
menu "Summer Mode Settings"
config TEMP_SUMMER_MODE_HIGH
int "Summer mode activation threshold [°C x 10]"
range 150 300
default 200
help
Outdoor temperature above which summer mode activates in 0.1°C units.
Example: 200 = 20.0°C
config TEMP_SUMMER_MODE_LOW
int "Summer mode deactivation threshold [°C x 10]"
range 100 250
default 150
help
Outdoor temperature below which summer mode deactivates in 0.1°C units.
Example: 150 = 15.0°C
endmenu
config BURNER_FAULT_DETECTION_SECONDS
int "Burner fault detection timeout (seconds)"
range 60 600
default 240
help
Time in seconds to wait before checking for burner fault after enabling.
endmenu
menu "Sensor Limits"
menu "Chamber Temperature Limits"
config SENSOR_LIMIT_CHAMBER_MAX
int "Chamber sensor maximum [°C x 10]"
range 500 1200
default 950
help
Maximum valid chamber temperature reading in 0.1°C units.
config SENSOR_LIMIT_CHAMBER_MIN
int "Chamber sensor minimum [°C x 10]"
range -400 100
default -100
help
Minimum valid chamber temperature reading in 0.1°C units.
endmenu
menu "Outdoor Temperature Limits"
config SENSOR_LIMIT_OUTDOOR_MAX
int "Outdoor sensor maximum [°C x 10]"
range 300 600
default 450
help
Maximum valid outdoor temperature reading in 0.1°C units.
config SENSOR_LIMIT_OUTDOOR_MIN
int "Outdoor sensor minimum [°C x 10]"
range -500 0
default -200
help
Minimum valid outdoor temperature reading in 0.1°C units.
endmenu
menu "Inlet Flow Temperature Limits"
config SENSOR_LIMIT_INLET_MAX
int "Inlet flow sensor maximum [°C x 10]"
range 500 1200
default 950
help
Maximum valid inlet flow temperature reading in 0.1°C units.
config SENSOR_LIMIT_INLET_MIN
int "Inlet flow sensor minimum [°C x 10]"
range -400 100
default -100
help
Minimum valid inlet flow temperature reading in 0.1°C units.
endmenu
menu "Return Flow Temperature Limits"
config SENSOR_LIMIT_RETURN_MAX
int "Return flow sensor maximum [°C x 10]"
range 500 1200
default 950
help
Maximum valid return flow temperature reading in 0.1°C units.
config SENSOR_LIMIT_RETURN_MIN
int "Return flow sensor minimum [°C x 10]"
range -400 100
default -100
help
Minimum valid return flow temperature reading in 0.1°C units.
endmenu
config SENSOR_GRACE_PERIOD_MINUTES
int "Sensor unchanged grace period (minutes)"
range 1 120
default 30
help
Maximum time in minutes a sensor can report unchanged values
before being flagged as faulty.
endmenu
menu "Damping Factors"
config DAMPING_FACTOR_WARMER
int "Damping factor warmer [x 0.00001]"
range 1 100
default 1
help
Damping factor for rising temperatures in units of 0.00001.
Example: 1 = 0.00001 (0.001%)
config DAMPING_FACTOR_COLDER
int "Damping factor colder [x 0.00001]"
range 1 100
default 5
help
Damping factor for falling temperatures in units of 0.00001.
Example: 5 = 0.00005 (0.005%)
endmenu
menu "Heating Schedule"
menu "Weekday Schedule (Monday-Thursday)"
config SCHEDULE_WEEKDAY_DAY_START_HOUR
int "Day mode start hour"
range 0 23
default 4
help
Hour when day mode starts on weekdays (24h format).
config SCHEDULE_WEEKDAY_DAY_START_MINUTE
int "Day mode start minute"
range 0 59
default 45
help
Minute when day mode starts on weekdays.
config SCHEDULE_WEEKDAY_NIGHT_START_HOUR
int "Night mode start hour"
range 0 23
default 22
help
Hour when night mode starts on weekdays (24h format).
config SCHEDULE_WEEKDAY_NIGHT_START_MINUTE
int "Night mode start minute"
range 0 59
default 0
help
Minute when night mode starts on weekdays.
endmenu
menu "Friday Schedule"
config SCHEDULE_FRIDAY_DAY_START_HOUR
int "Day mode start hour"
range 0 23
default 4
help
Hour when day mode starts on Friday (24h format).
config SCHEDULE_FRIDAY_DAY_START_MINUTE
int "Day mode start minute"
range 0 59
default 45
help
Minute when day mode starts on Friday.
config SCHEDULE_FRIDAY_NIGHT_START_HOUR
int "Night mode start hour"
range 0 23
default 23
help
Hour when night mode starts on Friday (24h format).
config SCHEDULE_FRIDAY_NIGHT_START_MINUTE
int "Night mode start minute"
range 0 59
default 0
help
Minute when night mode starts on Friday.
endmenu
menu "Saturday Schedule"
config SCHEDULE_SATURDAY_DAY_START_HOUR
int "Day mode start hour"
range 0 23
default 6
help
Hour when day mode starts on Saturday (24h format).
config SCHEDULE_SATURDAY_DAY_START_MINUTE
int "Day mode start minute"
range 0 59
default 45
help
Minute when day mode starts on Saturday.
config SCHEDULE_SATURDAY_NIGHT_START_HOUR
int "Night mode start hour"
range 0 23
default 23
help
Hour when night mode starts on Saturday (24h format).
config SCHEDULE_SATURDAY_NIGHT_START_MINUTE
int "Night mode start minute"
range 0 59
default 30
help
Minute when night mode starts on Saturday.
endmenu
menu "Sunday Schedule"
config SCHEDULE_SUNDAY_DAY_START_HOUR
int "Day mode start hour"
range 0 23
default 6
help
Hour when day mode starts on Sunday (24h format).
config SCHEDULE_SUNDAY_DAY_START_MINUTE
int "Day mode start minute"
range 0 59
default 45
help
Minute when day mode starts on Sunday.
config SCHEDULE_SUNDAY_NIGHT_START_HOUR
int "Night mode start hour"
range 0 23
default 22
help
Hour when night mode starts on Sunday (24h format).
config SCHEDULE_SUNDAY_NIGHT_START_MINUTE
int "Night mode start minute"
range 0 59
default 30
help
Minute when night mode starts on Sunday.
endmenu
endmenu
endmenu endmenu

View File

@ -1,145 +1,134 @@
/**
* @file control.c
* @brief Implementation of heating control module.
*/
#include "control.h" #include "control.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "inputs.h" #include "inputs.h"
#include "outputs.h" #include "outputs.h"
#include "safety.h" #include "safety.h"
#include "sntp.h" #include "sntp.h"
#include "esp_log.h" #define PERIODIC_INTERVAL 1U // Run control loop every 1 second
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <time.h> // Temperature thresholds
#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY 30.0f
#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT 25.0f
#define CHAMBER_TEMPERATURE_TARGET 80.0f // Max cutoff temperature
#define CHAMBER_TEMPERATURE_THRESHOLD 45.0f // Min threshold for burner enable
#define SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH \
20.0f // Summer mode will be activated
#define SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW \
15.0f // Summer mode will be deactivated --> Heating starts
#define CIRCULATION_PUMP_TEMPERATURE_THRESHOLD \
30.0f // Min threshold of chamber for circulation pump enable
#define BURNER_FAULT_DETECTION_THRESHOLD \
(60U * 4U) // Burner fault detection after 4 minutes
/** @brief Task interval in seconds. */ static const char *TAG = "smart-oil-heater-control-system-control";
#define PERIODIC_INTERVAL 1U static eControlState sControlState = CONTROL_STARTING;
// Control table for daily schedules
static const char *TAG = "control"; static const sControlDay aControlTable[] = {
static eControlState gControlState = CONTROL_STARTING;
/** @brief Weekly schedule table (from Kconfig). */
static const sControlDay gControlTable[] = {
{MONDAY, {MONDAY,
2U, 2U,
{{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE}, {{{4, 45},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE}, {{22, 0},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{TUESDAY, {TUESDAY,
2U, 2U,
{{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE}, {{{4, 45},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE}, {{22, 0},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{WEDNESDAY, {WEDNESDAY,
2U, 2U,
{{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE}, {{{4, 45},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE}, {{22, 0},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{THURSDAY, {THURSDAY,
2U, 2U,
{{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE}, {{{4, 45},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE}, {{22, 0},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{FRIDAY, {FRIDAY,
2U, 2U,
{{{CONFIG_SCHEDULE_FRIDAY_DAY_START_HOUR, CONFIG_SCHEDULE_FRIDAY_DAY_START_MINUTE}, {{{4, 45},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_FRIDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_FRIDAY_NIGHT_START_MINUTE}, {{23, 0},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{SATURDAY, {SATURDAY,
2U, 2U,
{{{CONFIG_SCHEDULE_SATURDAY_DAY_START_HOUR, CONFIG_SCHEDULE_SATURDAY_DAY_START_MINUTE}, {{{6, 45},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_SATURDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_SATURDAY_NIGHT_START_MINUTE}, {{23, 30},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{SUNDAY, {SUNDAY,
2U, 2U,
{{{CONFIG_SCHEDULE_SUNDAY_DAY_START_HOUR, CONFIG_SCHEDULE_SUNDAY_DAY_START_MINUTE}, {{{6, 45},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_SUNDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_SUNDAY_NIGHT_START_MINUTE}, {{22, 30},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
}; };
static sControlTemperatureEntry currentControlEntry =
aControlTable[0].aTemperatureEntries[0];
static sControlTemperatureEntry gCurrentControlEntry = // Function prototypes
gControlTable[0].aTemperatureEntries[0]; void taskControl(void *pvParameters);
static SemaphoreHandle_t xMutexAccessControl = NULL; void findControlCurrentTemperatureEntry(void);
/* Private function prototypes */ void initControl(void)
static void taskControl(void *pvParameters);
static void findControlCurrentTemperatureEntry(void);
static void setControlState(eControlState state);
esp_err_t initControl(void)
{ {
xMutexAccessControl = xSemaphoreCreateRecursiveMutex(); BaseType_t taskCreated =
if (xMutexAccessControl == NULL) 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)
);
if (taskCreated == pdPASS)
{ {
ESP_LOGE(TAG, "Failed to create mutex"); ESP_LOGI(TAG, "Task created successfully!");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessControl); else
BaseType_t taskCreated = xTaskCreate(
taskControl,
"taskControl",
8192,
NULL,
5,
NULL);
if (taskCreated != pdPASS)
{ {
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;
eBurnerState burnerState = BURNER_UNKNOWN; eBurnerState eBurnerState = BURNER_UNKNOWN;
int64_t i64BurnerEnableTimestamp = esp_timer_get_time(); int64_t i64BurnerEnableTimestamp = esp_timer_get_time();
while (1) while (1)
{ {
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!");
setControlState(CONTROL_FAULT_SAFETY); sControlState = CONTROL_FAULT_SAFETY;
if (bHeatingInAction) if (bHeatingInAction)
{ {
ESP_LOGW(TAG, "Disabling burner due to safety fault"); ESP_LOGW(TAG, "Disabling burner due to safety fault");
@ -150,11 +139,11 @@ static 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!");
setControlState(CONTROL_FAULT_SNTP); sControlState = CONTROL_FAULT_SNTP;
if (bHeatingInAction) if (bHeatingInAction)
{ {
ESP_LOGW(TAG, "Disabling burner due to SNTP fault"); ESP_LOGW(TAG, "Disabling burner due to SNTP fault");
@ -166,51 +155,59 @@ static void taskControl(void *pvParameters)
} }
findControlCurrentTemperatureEntry(); findControlCurrentTemperatureEntry();
sControlTemperatureEntry currentControlEntry =
getControlCurrentTemperatureEntry();
/* Summer mode hysteresis */ if (getOutdoorTemperature().fDampedValue >=
if (getOutdoorTemperature().fDampedValue >= SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH) SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH)
{ {
bSummerMode = true; bSummerMode = true;
} }
else if (getOutdoorTemperature().fDampedValue <= SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW) else if (getOutdoorTemperature().fDampedValue <=
SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW)
{ {
bSummerMode = false; bSummerMode = false;
} }
/* Enable burner if needed */ // Enable burner if outdoor temperature is low and return flow temperature
if (!bHeatingInAction && (burnerState != BURNER_FAULT)) // is cooled down
if (!bHeatingInAction && (eBurnerState != 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); sControlState = CONTROL_OUTDOOR_TOO_WARM;
} }
else if ((getReturnFlowTemperature().average60s.fValue <= else if ((getReturnFlowTemperature().average60s.fValue <=
getControlCurrentTemperatureEntry().fReturnFlowTemperature) && currentControlEntry.fReturnFlowTemperature) &&
(getChamberTemperature().fCurrentValue <= CHAMBER_TEMPERATURE_THRESHOLD)) (getChamberTemperature().fCurrentValue <=
CHAMBER_TEMPERATURE_THRESHOLD))
{ {
ESP_LOGI(TAG, "Enabling burner: Return flow temperature target reached"); ESP_LOGI(TAG,
burnerState = BURNER_UNKNOWN; "Enabling burner: Return flow temperature target reached");
eBurnerState = BURNER_UNKNOWN;
bHeatingInAction = true; bHeatingInAction = true;
setBurnerState(ENABLED); setBurnerState(ENABLED);
setSafetyControlState(ENABLED); setSafetyControlState(ENABLED);
i64BurnerEnableTimestamp = esp_timer_get_time(); i64BurnerEnableTimestamp = esp_timer_get_time();
setControlState(CONTROL_HEATING); sControlState = CONTROL_HEATING;
} }
else else
{ {
setControlState(CONTROL_RETURN_FLOW_TOO_WARM); // ESP_LOGI(TAG, "Return flow temperature too warm: Disabling heating");
sControlState = CONTROL_RETURN_FLOW_TOO_WARM;
} }
} }
/* Disable burner if target reached or fault */ // Disable burner if target temperature is reached or a fault occurred
if (bHeatingInAction) if (bHeatingInAction)
{ {
if ((getChamberTemperature().fCurrentValue >= if ((getChamberTemperature().fCurrentValue >=
getControlCurrentTemperatureEntry().fChamberTemperature) || currentControlEntry.fChamberTemperature) ||
(getChamberTemperature().predict60s.fValue >= (getChamberTemperature().predict60s.fValue >=
getControlCurrentTemperatureEntry().fChamberTemperature)) currentControlEntry.fChamberTemperature))
{ {
ESP_LOGI(TAG, "Chamber target temperature reached: Disabling burner"); ESP_LOGI(TAG, "Chamber target temperature reached: Disabling burner");
bHeatingInAction = false; bHeatingInAction = false;
@ -220,14 +217,14 @@ static void taskControl(void *pvParameters)
else if (esp_timer_get_time() - i64BurnerEnableTimestamp >= else if (esp_timer_get_time() - i64BurnerEnableTimestamp >=
BURNER_FAULT_DETECTION_THRESHOLD * 1000000U) BURNER_FAULT_DETECTION_THRESHOLD * 1000000U)
{ {
if (burnerState == BURNER_UNKNOWN) if (eBurnerState == BURNER_UNKNOWN)
{ {
if (getBurnerError() == FAULT) if (getBurnerError() == FAULT)
{ {
// ESP_LOGW(TAG, "Burner fault detected: Disabling burner"); // ESP_LOGW(TAG, "Burner fault detected: Disabling burner");
bHeatingInAction = false; bHeatingInAction = false;
burnerState = BURNER_FAULT; eBurnerState = BURNER_FAULT;
setControlState(CONTROL_FAULT_BURNER); sControlState = CONTROL_FAULT_BURNER;
setBurnerState(DISABLED); setBurnerState(DISABLED);
setSafetyControlState(ENABLED); setSafetyControlState(ENABLED);
} }
@ -235,14 +232,15 @@ static void taskControl(void *pvParameters)
{ {
// ESP_LOGI(TAG, "No burner fault detected: Marking burner as // ESP_LOGI(TAG, "No burner fault detected: Marking burner as
// fired"); // fired");
burnerState = BURNER_FIRED; eBurnerState = BURNER_FIRED;
} }
} }
} }
} }
/* Manage circulation pump */ // Manage circulation pump
if (getChamberTemperature().fCurrentValue <= CIRCULATION_PUMP_TEMPERATURE_THRESHOLD) if (getChamberTemperature().fCurrentValue <=
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);
@ -255,58 +253,39 @@ static void taskControl(void *pvParameters)
} // End of while(1) } // End of while(1)
} }
/** eControlState getControlState(void) { return sControlState; }
* @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;
xSemaphoreGiveRecursive(xMutexAccessControl);
}
else
{
ESP_LOGE(TAG, "Unable to take mutex: setControlState()");
}
}
eControlState getControlState(void)
{
eControlState ret = CONTROL_FAULT_SAFETY;
if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE)
{
ret = gControlState;
xSemaphoreGiveRecursive(xMutexAccessControl);
}
else
{
ESP_LOGE(TAG, "Unable to take mutex: getControlState()");
}
return ret;
}
eControlWeekday getControlCurrentWeekday(void) eControlWeekday getControlCurrentWeekday(void)
{ {
time_t now; time_t now;
struct tm timeinfo; struct tm *timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
int day = timeinfo.tm_wday; time(&now);
timeinfo = localtime(&now);
int day = timeinfo->tm_wday;
return (eControlWeekday)((day == 0) ? 6 : day - 1); return (eControlWeekday)((day == 0) ? 6 : day - 1);
} }
/** /**
* @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.
*/ */
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);
@ -315,88 +294,66 @@ static void findControlCurrentTemperatureEntry(void)
int currentHour = timeinfo.tm_hour; int currentHour = timeinfo.tm_hour;
int currentMinute = timeinfo.tm_min; int currentMinute = timeinfo.tm_min;
if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE) // ESP_LOGI(TAG, "Searching for control entry - Day: %d, Time: %02d:%02d", currentDay, currentHour, currentMinute);
// Search through all days and entries
for (int dayIndex = 0; dayIndex < 7; dayIndex++)
{ {
const sControlDay *day = &aControlTable[dayIndex];
// ESP_LOGI(TAG, "Searching for control entry - Day: %d, Time: %02d:%02d", currentDay, currentHour, currentMinute); for (int entryIndex = 0; entryIndex < day->entryCount; entryIndex++)
// Search through all days and entries
for (int dayIndex = 0; dayIndex < 7; dayIndex++)
{ {
const sControlDay *day = &gControlTable[dayIndex]; const sControlTemperatureEntry *entry = &day->aTemperatureEntries[entryIndex];
for (int entryIndex = 0; entryIndex < day->entryCount; entryIndex++) // Check if this entry is in the future (next active entry)
bool isFutureDay = (day->day > currentDay);
bool isTodayFutureTime = (day->day == currentDay) &&
((entry->timestamp.hour > currentHour) ||
(entry->timestamp.hour == currentHour &&
entry->timestamp.minute > currentMinute));
if (isFutureDay || isTodayFutureTime)
{ {
const sControlTemperatureEntry *entry = &day->aTemperatureEntries[entryIndex]; // Found next scheduled entry, so determine the previous (active) one
if (entryIndex > 0)
// Check if this entry is in the future (next active entry)
bool isFutureDay = (day->day > currentDay);
bool isTodayFutureTime = (day->day == currentDay) &&
((entry->timestamp.hour > currentHour) ||
(entry->timestamp.hour == currentHour &&
entry->timestamp.minute > currentMinute));
if (isFutureDay || isTodayFutureTime)
{ {
// Use previous entry from same day
// Found next scheduled entry, so determine the previous (active) one currentControlEntry = day->aTemperatureEntries[entryIndex - 1];
if (entryIndex > 0)
{
// Use previous entry from same day
gCurrentControlEntry = day->aTemperatureEntries[entryIndex - 1];
}
else if (dayIndex > 0)
{
// Use last entry from previous day
const sControlDay *previousDay = &gControlTable[dayIndex - 1];
gCurrentControlEntry = previousDay->aTemperatureEntries[previousDay->entryCount - 1];
}
else
{
// First entry of the week - wrap to last entry of Sunday
const sControlDay *sunday = &gControlTable[6];
gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
}
xSemaphoreGiveRecursive(xMutexAccessControl);
/*
ESP_LOGI(TAG, "Active entry found - Time: %02d:%02d, "
"Return Temp: %lf, Chamber Temp: %lf",
gCurrentControlEntry.timestamp.hour,
gCurrentControlEntry.timestamp.minute,
gCurrentControlEntry.fReturnFlowTemperature,
gCurrentControlEntry.fChamberTemperature);
*/
return;
} }
else if (dayIndex > 0)
{
// Use last entry from previous day
const sControlDay *previousDay = &aControlTable[dayIndex - 1];
currentControlEntry = previousDay->aTemperatureEntries[previousDay->entryCount - 1];
}
else
{
// First entry of the week - wrap to last entry of Sunday
const sControlDay *sunday = &aControlTable[6];
currentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
}
/*
ESP_LOGI(TAG, "Active entry found - Time: %02d:%02d, "
"Return Temp: %lf, Chamber Temp: %lf",
currentControlEntry.timestamp.hour,
currentControlEntry.timestamp.minute,
currentControlEntry.fReturnFlowTemperature,
currentControlEntry.fChamberTemperature);
*/
return;
} }
} }
// If we reached here, current time is after all entries this week
// Use the last entry (Sunday evening)
const sControlDay *sunday = &gControlTable[6];
gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
// ESP_LOGI(TAG, "Using last entry of week - Time: %02d:%02d", gCurrentControlEntry.timestamp.hour, gCurrentControlEntry.timestamp.minute);
xSemaphoreGiveRecursive(xMutexAccessControl);
}
else
{
ESP_LOGE(TAG, "Unable to take mutex: findControlCurrentTemperatureEntry()");
} }
// If we reached here, current time is after all entries this week
// Use the last entry (Sunday evening)
const sControlDay *sunday = &aControlTable[6];
currentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
// ESP_LOGI(TAG, "Using last entry of week - Time: %02d:%02d", currentControlEntry.timestamp.hour, currentControlEntry.timestamp.minute);
} }
sControlTemperatureEntry getControlCurrentTemperatureEntry(void) sControlTemperatureEntry getControlCurrentTemperatureEntry(void)
{ {
sControlTemperatureEntry ret = gControlTable[0].aTemperatureEntries[0]; return currentControlEntry;
if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE)
{
ret = gCurrentControlEntry;
xSemaphoreGiveRecursive(xMutexAccessControl);
}
else
{
ESP_LOGE(TAG, "Unable to take mutex: getControlCurrentTemperatureEntry()");
}
return ret;
} }

View File

@ -1,74 +1,26 @@
/**
* @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 <time.h>
#include "sdkconfig.h"
#include "esp_err.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)
/** @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 typedef enum _ControlState
{ {
CONTROL_STARTING, /**< System starting up. */ CONTROL_STARTING,
CONTROL_HEATING, /**< Burner running. */ CONTROL_HEATING,
CONTROL_OUTDOOR_TOO_WARM, /**< Summer mode active. */ CONTROL_OUTDOOR_TOO_WARM,
CONTROL_RETURN_FLOW_TOO_WARM, /**< Target temperature reached. */ CONTROL_RETURN_FLOW_TOO_WARM,
CONTROL_FAULT_BURNER, /**< Burner fault detected. */ CONTROL_FAULT_BURNER,
CONTROL_FAULT_SAFETY, /**< Safety fault detected. */ CONTROL_FAULT_SAFETY,
CONTROL_FAULT_SNTP, /**< SNTP sync failed. */ CONTROL_FAULT_SNTP,
} eControlState; } eControlState;
/**
* @brief Burner operational state enumeration.
*/
typedef enum _BurnerState typedef enum _BurnerState
{ {
BURNER_UNKNOWN, /**< Burner state unknown after enable. */ BURNER_UNKNOWN, // Burner is disabled or state after enabling is still unkown
BURNER_FIRED, /**< Burner fired successfully. */ BURNER_FIRED, // Burner fired successfully
BURNER_FAULT /**< Burner failed to fire. */ BURNER_FAULT // Burner was unable to fire successfully
} eBurnerState; } eBurnerState;
/**
* @brief Weekday enumeration (Monday = 0).
*/
typedef enum _ControlWeekday typedef enum _ControlWeekday
{ {
MONDAY, MONDAY,
@ -80,58 +32,27 @@ typedef enum _ControlWeekday
SUNDAY, SUNDAY,
} eControlWeekday; } eControlWeekday;
/**
* @brief Time of day structure.
*/
typedef struct _ControlTimestamp typedef struct _ControlTimestamp
{ {
uint8_t hour; /**< Hour (0-23). */ uint8_t hour;
uint8_t minute; /**< Minute (0-59). */ uint8_t minute;
} sControlTimestamp; } sControlTimestamp;
/**
* @brief Temperature schedule entry.
*/
typedef struct _ControlTemperatureEntry typedef struct _ControlTemperatureEntry
{ {
sControlTimestamp timestamp; /**< Time when entry becomes active. */ sControlTimestamp timestamp;
float fReturnFlowTemperature; /**< Target return flow temperature. */ float fReturnFlowTemperature;
float fChamberTemperature; /**< Target chamber temperature. */ float fChamberTemperature;
} sControlTemperatureEntry; } sControlTemperatureEntry;
/**
* @brief Daily schedule structure.
*/
typedef struct _ControlDay typedef struct _ControlDay
{ {
eControlWeekday day; /**< Day of week. */ eControlWeekday day;
size_t entryCount; /**< Number of entries for this day. */ size_t entryCount; // number of entries for each day
sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY]; /**< Schedule entries. */ sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY];
} 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,51 +1,29 @@
/**
* @file inputs.c
* @brief Implementation of input handling module.
*/
#include "inputs.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#include <string.h>
#include <math.h>
#include "esp_log.h" #include "esp_log.h"
#include <ds18x20.h> #include <ds18x20.h>
#include <string.h> #include "inputs.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
/** @brief Number of retry attempts for 1-Wire read. */ static const char *TAG = "smart-oil-heater-control-system-inputs";
#define ONE_WIRE_LOOPS 4U const uint8_t uBurnerFaultPin = 19U;
const uint8_t uDS18B20Pin = 4U;
/** @brief Task interval in seconds. */ const onewire_addr_t uChamperTempSensorAddr = 0xd00000108cd01d28;
#define PERIODIC_INTERVAL 1U const onewire_addr_t uOutdoorTempSensorAddr = 0xd70000108a9b9128;
const onewire_addr_t uInletFlowTempSensorAddr = 0x410000108b8c0628;
const onewire_addr_t uReturnFlowTempSensorAddr = 0x90000108cc77c28;
static const char *TAG = "inputs"; onewire_addr_t uOneWireAddresses[MAX_DN18B20_SENSORS];
float fDS18B20Temps[MAX_DN18B20_SENSORS];
/** @brief Burner fault GPIO pin (from Kconfig). */ size_t sSensorCount = 0U;
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;
@ -54,34 +32,29 @@ static sMeasurement sOutdoorTemperature;
static sMeasurement sInletFlowTemperature; static sMeasurement sInletFlowTemperature;
static sMeasurement sReturnFlowTemperature; static sMeasurement sReturnFlowTemperature;
/* Private function prototypes */ void taskInput(void *pvParameters);
static void taskInput(void *pvParameters); void initMeasurement(sMeasurement *pMeasurement);
static void initMeasurement(sMeasurement *pMeasurement); void updateAverage(sMeasurement *pMeasurement);
static void updateAverage(sMeasurement *pMeasurement); void updatePrediction(sMeasurement *pMeasurement);
static void updatePrediction(sMeasurement *pMeasurement); float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex);
static float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex);
esp_err_t initInputs(void) void initInputs(void)
{ {
gpio_config_t ioConfBurnerFault = {
.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); gpio_config_t ioConfBurnerFault = {
if (ret != ESP_OK) .pin_bit_mask = (1ULL << uBurnerFaultPin), // Pin mask
{ .mode = GPIO_MODE_INPUT, // Set as inout
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret)); .pull_up_en = GPIO_PULLUP_ENABLE, // Enable pull-up
return ESP_FAIL; .pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down
} .intr_type = GPIO_INTR_DISABLE // Disable interrupts
};
gpio_config(&ioConfBurnerFault);
xMutexAccessInputs = xSemaphoreCreateRecursiveMutex(); xMutexAccessInputs = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessInputs == NULL) if (xMutexAccessInputs == NULL)
{ {
ESP_LOGE(TAG, "Failed to create mutex"); ESP_LOGE(TAG, "Unable to create mutex");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessInputs); xSemaphoreGiveRecursive(xMutexAccessInputs);
@ -91,28 +64,25 @@ esp_err_t initInputs(void)
initMeasurement(&sReturnFlowTemperature); initMeasurement(&sReturnFlowTemperature);
BaseType_t taskCreated = xTaskCreate( BaseType_t taskCreated = xTaskCreate(
taskInput, taskInput, // Function to implement the task
"taskInput", "taskInput", // Task name
4096, 4096, // Stack size (in words, not bytes)
NULL, NULL, // Parameters to the task function (none in this case)
5, 5, // Task priority (higher number = higher priority)
NULL); NULL // Task handle (optional)
);
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;
@ -124,29 +94,25 @@ static void initMeasurement(sMeasurement *pMeasurement)
pMeasurement->average10s.fValue = INITIALISATION_VALUE; pMeasurement->average10s.fValue = INITIALISATION_VALUE;
pMeasurement->average10s.bufferCount = 0U; pMeasurement->average10s.bufferCount = 0U;
pMeasurement->average10s.bufferIndex = 0U; pMeasurement->average10s.bufferIndex = 0U;
memset(pMeasurement->average10s.samples, 0U, sizeof(float) * AVG10S_SAMPLE_SIZE); memset(pMeasurement->average10s.samples, 0U, AVG10S_SAMPLE_SIZE);
pMeasurement->average60s.fValue = INITIALISATION_VALUE; pMeasurement->average60s.fValue = INITIALISATION_VALUE;
pMeasurement->average60s.bufferCount = 0U; pMeasurement->average60s.bufferCount = 0U;
pMeasurement->average60s.bufferIndex = 0U; pMeasurement->average60s.bufferIndex = 0U;
memset(pMeasurement->average60s.samples, 0U, sizeof(float) * AVG60S_SAMPLE_SIZE); memset(pMeasurement->average60s.samples, 0U, AVG60S_SAMPLE_SIZE);
pMeasurement->predict60s.fValue = INITIALISATION_VALUE; pMeasurement->predict60s.fValue = INITIALISATION_VALUE;
pMeasurement->predict60s.bufferCount = 0U; pMeasurement->predict60s.bufferCount = 0U;
pMeasurement->predict60s.bufferIndex = 0U; pMeasurement->predict60s.bufferIndex = 0U;
memset(pMeasurement->predict60s.samples, 0U, sizeof(float) * PRED60S_SAMPLE_SIZE); memset(pMeasurement->predict60s.samples, 0U, 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;
/* 10-second average */ // Average form the last 10sec
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;
@ -156,21 +122,14 @@ static void updateAverage(sMeasurement *pMeasurement)
} }
float sum = 0.0; float sum = 0.0;
for (int i = 0; i < pMeasurement->average10s.bufferCount; i++) for (int i = 0; i <= pMeasurement->average10s.bufferCount; i++)
{ {
sum += pMeasurement->average10s.samples[i]; sum += pMeasurement->average10s.samples[i];
} }
if (pMeasurement->average10s.bufferCount == 0U) pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount;
{
pMeasurement->average10s.fValue = 0.0f;
}
else
{
pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount;
}
/* 60-second average */ // Average form the last 60sec
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;
@ -185,16 +144,9 @@ static void updateAverage(sMeasurement *pMeasurement)
sum += pMeasurement->average60s.samples[i]; sum += pMeasurement->average60s.samples[i];
} }
if (pMeasurement->average60s.bufferCount == 0U) pMeasurement->average60s.fValue = sum / pMeasurement->average60s.bufferCount;
{
pMeasurement->average60s.fValue = 0.0f;
}
else
{
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;
@ -213,11 +165,7 @@ static 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;
@ -237,11 +185,7 @@ static 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)
{ {
@ -343,27 +287,20 @@ static 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; return INITIALISATION_VALUE; // No prediction possible with no data
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; float x = (float)i; // Time index
float y = samples[circularIndex]; float y = samples[circularIndex]; // Sample value
sumX += x; sumX += x;
sumY += y; sumY += y;
@ -371,13 +308,15 @@ static float linearRegressionPredict(const float *samples, size_t count, size_t
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) if (fabs(denominator) < 1e-6) // Avoid division by zero
return samples[bufferIndex]; return samples[bufferIndex]; // Return the latest value as prediction
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;
} }
@ -458,4 +397,4 @@ eBurnerErrorState getBurnerError(void)
ESP_LOGE(TAG, "Unable to take mutex: getBurnerError()"); ESP_LOGE(TAG, "Unable to take mutex: getBurnerError()");
} }
return ret; return ret;
} }

View File

@ -1,133 +1,55 @@
/**
* @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 "esp_err.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
#define DAMPING_FACTOR_WARMER 0.00001f // 0.001%
#define DAMPING_FACTOR_COLDER 0.00005f // 0.005%
/** @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 typedef enum _BurnerErrorState
{ {
NO_ERROR, /**< No burner fault detected. */ NO_ERROR,
FAULT /**< Burner fault signal active. */ FAULT
} eBurnerErrorState; } eBurnerErrorState;
/**
* @brief Measurement error state enumeration.
*/
typedef enum _MeasurementErrorState typedef enum _MeasurementErrorState
{ {
MEASUREMENT_NO_ERROR, /**< Measurement valid. */ MEASUREMENT_NO_ERROR,
MEASUREMENT_FAULT /**< Measurement failed or sensor not found. */ MEASUREMENT_FAULT
} eMeasurementErrorState; } eMeasurementErrorState;
/**
* @brief Circular buffer for averaging temperature values.
*/
typedef struct _Average typedef struct _Average
{ {
float fValue; /**< Current average value. */ float fValue;
float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))]; /**< Sample buffer. */ float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))];
size_t bufferIndex; /**< Current write index. */ size_t bufferIndex;
size_t bufferCount; /**< Number of valid samples. */ size_t bufferCount;
} sAverage; } sAverage;
/**
* @brief Circular buffer for temperature prediction.
*/
typedef struct _Predict typedef struct _Predict
{ {
float fValue; /**< Predicted value. */ float fValue;
float samples[PRED60S_SAMPLE_SIZE]; /**< Sample buffer. */ float samples[PRED60S_SAMPLE_SIZE];
size_t bufferIndex; /**< Current write index. */ size_t bufferIndex;
size_t bufferCount; /**< Number of valid samples. */ size_t bufferCount;
} sPredict; } sPredict;
/**
* @brief Complete measurement data structure.
*/
typedef struct _Measurement typedef struct _Measurement
{ {
float fCurrentValue; /**< Current raw temperature value. */ float fCurrentValue;
float fDampedValue; /**< Damped temperature value. */ float fDampedValue;
sAverage average10s; /**< 10-second rolling average. */ sAverage average10s;
sAverage average60s; /**< 60-second rolling average. */ sAverage average60s;
sPredict predict60s; /**< 60-second prediction. */ sPredict predict60s;
eMeasurementErrorState state; /**< Measurement state. */ eMeasurementErrorState 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);
eBurnerErrorState getBurnerError(void);
/**
* @brief Get the current burner error state.
* @return eBurnerErrorState indicating fault status.
*/
eBurnerErrorState getBurnerError(void);

View File

@ -1,10 +1,6 @@
/** #include "esp_log.h"
* @file main.c #include <esp_system.h>
* @brief Main entry point for Smart Oil Heating Control System. #include "nvs_flash.h"
*
* 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"
@ -14,114 +10,33 @@
#include "wifi.h" #include "wifi.h"
#include "sntp.h" #include "sntp.h"
#include "esp_log.h" static const char *TAG = "smart-oil-heater-control-system";
#include "esp_system.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
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, "========================================"); ESP_LOGI(TAG, "starting ...");
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();
} }
if (ret != ESP_OK) ESP_ERROR_CHECK(ret);
{
ESP_LOGE(TAG, "NVS init failed: %s", esp_err_to_name(ret));
reboot_on_error("NVS");
}
ESP_LOGI(TAG, "[OK] NVS initialized");
/* Initialize Outputs */ // TODO: Error handling!
if (initOutputs() != ESP_OK) initOutputs();
{ initInputs();
reboot_on_error("Outputs"); initSafety();
} initWifi();
ESP_LOGI(TAG, "[OK] Outputs initialized"); initSntp();
initControl();
/* Initialize Inputs */ initMetrics();
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)
{ {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
// Do nothing ;-) // Do nothing ;-)
} }
} }

View File

@ -1,7 +1,11 @@
/** #include <string.h>
* @file metrics.c #include "esp_timer.h"
* @brief Implementation of Prometheus metrics endpoint. #include "freertos/FreeRTOS.h"
*/ #include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include <time.h>
#include <sys/time.h>
#include "metrics.h" #include "metrics.h"
#include "outputs.h" #include "outputs.h"
@ -10,60 +14,41 @@
#include "sntp.h" #include "sntp.h"
#include "control.h" #include "control.h"
#include "esp_timer.h" static const char *TAG = "smart-oil-heater-control-system-metrics";
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include <string.h> char caHtmlResponse[HTML_RESPONSE_SIZE];
#include <time.h> SemaphoreHandle_t xMutexAccessMetricResponse = NULL;
#include <sys/time.h>
static const char *TAG = "metrics";
static char caHtmlResponse[HTML_RESPONSE_SIZE];
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;
/* Private function prototypes */ void taskMetrics(void *pvParameters);
static void taskMetrics(void *pvParameters); httpd_handle_t setup_server(void);
static httpd_handle_t setup_server(void); esp_err_t get_metrics_handler(httpd_req_t *req);
static esp_err_t get_metrics_handler(httpd_req_t *req);
esp_err_t initMetrics(void) void initMetrics(void)
{ {
httpd_handle_t server = setup_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, taskMetrics, // Function to implement the task
"taskMetrics", "taskMetrics", // Task name
32768, 32768, // Stack size (in words, not bytes)
NULL, NULL, // Parameters to the task function (none in this case)
5, 5, // Task priority (higher number = higher priority)
NULL); NULL // Task handle (optional)
);
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)
{ {
@ -316,23 +301,23 @@ static void taskMetrics(void *pvParameters)
// Wifi RSSI // Wifi RSSI
wifi_ap_record_t ap; wifi_ap_record_t ap;
ap.rssi = 0U; esp_wifi_sta_get_ap_info(&ap);
ESP_ERROR_CHECK(esp_wifi_sta_get_ap_info(&ap));
strcpy(aMetrics[u16MetricCounter].caMetricName, "wifi_rssi"); strcpy(aMetrics[u16MetricCounter].caMetricName, "wifi_rssi");
aMetrics[u16MetricCounter].type = INTEGER_64; aMetrics[u16MetricCounter].type = INTEGER_64;
aMetrics[u16MetricCounter].i64MetricValue = ap.rssi; aMetrics[u16MetricCounter].i64MetricValue = ap.rssi;
u16MetricCounter++; u16MetricCounter++;
configASSERT(!(u16MetricCounter > METRIC_MAX_COUNT)); ESP_ERROR_CHECK(u16MetricCounter > METRIC_MAX_COUNT);
vSetMetrics(aMetrics, u16MetricCounter); vSetMetrics(aMetrics, u16MetricCounter);
} }
} }
void vSetMetrics(sMetric *paMetrics, uint16_t u16Size) void vSetMetrics(sMetric *paMetrics, uint16_t u16Size)
{ {
if (xSemaphoreTakeRecursive(xMutexAccessMetricResponse, pdMS_TO_TICKS(5000)) == pdTRUE) if (xSemaphoreTakeRecursive(xMutexAccessMetricResponse, pdMS_TO_TICKS(5000)) == pdTRUE)
{ {
memset(caHtmlResponse, 0U, HTML_RESPONSE_SIZE); memset(caHtmlResponse, 0U, strlen(caHtmlResponse));
for (uint16_t u16Index = 0U; u16Index < u16Size; u16Index++) for (uint16_t u16Index = 0U; u16Index < u16Size; u16Index++)
{ {
char caValueBuffer[64]; char caValueBuffer[64];
@ -352,6 +337,8 @@ 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");
@ -364,12 +351,7 @@ 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)
{ {
@ -384,11 +366,7 @@ static 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;
@ -403,17 +381,14 @@ static httpd_handle_t setup_server(void)
xMutexAccessMetricResponse = xSemaphoreCreateRecursiveMutex(); xMutexAccessMetricResponse = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessMetricResponse == NULL) if (xMutexAccessMetricResponse == NULL)
{ {
ESP_LOGE(TAG, "Failed to create mutex"); ESP_LOGE(TAG, "Unable to create mutex for metric response");
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;
} }
ESP_LOGE(TAG, "Failed to start HTTP server"); return server;
return NULL;
} }

View File

@ -1,61 +1,26 @@
/**
* @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_err.h" #include <esp_http_server.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, /**< Floating point value. */ FLOAT,
INTEGER_U8, /**< 8-bit unsigned integer. */ INTEGER_U8,
INTEGER_64, /**< 64-bit signed integer. */ INTEGER_64,
} eMetricValueType; } eMetricValueType;
/**
* @brief Metric data structure.
*/
typedef struct _metric typedef struct _metric
{ {
char caMetricName[METRIC_NAME_MAX_SIZE]; /**< Metric name. */ char caMetricName[METRIC_NAME_MAX_SIZE];
eMetricValueType type; /**< Value type. */ eMetricValueType type;
float fMetricValue; /**< Float value (if type is FLOAT). */ float fMetricValue;
uint8_t u8MetricValue; /**< U8 value (if type is INTEGER_U8). */ uint8_t u8MetricValue;
int64_t i64MetricValue; /**< I64 value (if type is INTEGER_64). */ int64_t i64MetricValue;
} sMetric; } sMetric;
/** void initMetrics(void);
* @brief Initialize the metrics module. void vSetMetrics(sMetric *paMetrics, uint16_t u16Size);
*
* 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,101 +1,61 @@
/**
* @file outputs.c
* @brief Implementation of output control module.
*/
#include "outputs.h"
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#include "esp_log.h" #include "esp_log.h"
static const char *TAG = "outputs"; #include "outputs.h"
/** @brief Circulation pump GPIO pin (from Kconfig). */ static const char *TAG = "smart-oil-heater-control-system-outputs";
static const uint8_t uCirculationPumpGpioPin = CONFIG_GPIO_CIRCULATION_PUMP; const uint8_t uCirculationPumpGpioPin = 27U;
const uint8_t uBurnerGpioPin = 14U;
/** @brief Burner control GPIO pin (from Kconfig). */ const uint8_t uSafetyContactGpioPin = 12U;
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;
esp_err_t initOutputs(void) void initOutputs(void)
{ {
gpio_config_t ioConfCirculationPump = { gpio_config_t ioConfCirculationPump = {
.pin_bit_mask = (1ULL << uCirculationPumpGpioPin), .pin_bit_mask = (1ULL << uCirculationPumpGpioPin), // Pin mask
.mode = GPIO_MODE_OUTPUT, .mode = GPIO_MODE_OUTPUT, // Set as output
.pull_up_en = GPIO_PULLUP_DISABLE, .pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up
.pull_down_en = GPIO_PULLDOWN_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down
.intr_type = GPIO_INTR_DISABLE}; .intr_type = GPIO_INTR_DISABLE // Disable interrupts
};
gpio_config_t ioConfBurner = { gpio_config_t ioConfBurner = {
.pin_bit_mask = (1ULL << uBurnerGpioPin), .pin_bit_mask = (1ULL << uBurnerGpioPin), // Pin mask
.mode = GPIO_MODE_OUTPUT, .mode = GPIO_MODE_OUTPUT, // Set as output
.pull_up_en = GPIO_PULLUP_DISABLE, .pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up
.pull_down_en = GPIO_PULLDOWN_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down
.intr_type = GPIO_INTR_DISABLE}; .intr_type = GPIO_INTR_DISABLE // Disable interrupts
};
gpio_config_t ioConfSafetyContact = { gpio_config_t ioConfSafetyContact = {
.pin_bit_mask = (1ULL << uSafetyContactGpioPin), .pin_bit_mask = (1ULL << uSafetyContactGpioPin), // Pin mask
.mode = GPIO_MODE_OUTPUT, .mode = GPIO_MODE_OUTPUT, // Set as output
.pull_up_en = GPIO_PULLUP_DISABLE, .pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up
.pull_down_en = GPIO_PULLDOWN_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down
.intr_type = GPIO_INTR_DISABLE}; .intr_type = GPIO_INTR_DISABLE // Disable interrupts
};
esp_err_t ret = gpio_config(&ioConfCirculationPump); gpio_config(&ioConfCirculationPump);
if (ret != ESP_OK) gpio_config(&ioConfBurner);
{ gpio_config(&ioConfSafetyContact);
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 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 for safety contact: %s", esp_err_to_name(ret));
return ESP_FAIL;
}
xMutexAccessOutputs = xSemaphoreCreateRecursiveMutex(); xMutexAccessOutputs = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessOutputs == NULL) if (xMutexAccessOutputs == NULL)
{ {
ESP_LOGE(TAG, "Failed to create mutex"); ESP_LOGE(TAG, "Unable to create mutex");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessOutputs); xSemaphoreGiveRecursive(xMutexAccessOutputs);
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
} }
eOutput getCirculationPumpState(void) eOutput getCirculationPumpState(void)
{ {
eOutput ret = ENABLED; return sCirculationPumpState;
if (xSemaphoreTakeRecursive(xMutexAccessOutputs, pdMS_TO_TICKS(5000)) == pdTRUE)
{
ret = sCirculationPumpState;
xSemaphoreGiveRecursive(xMutexAccessOutputs);
}
else
{
ESP_LOGE(TAG, "Unable to take mutex: getCirculationPumpState()");
}
return ret;
} }
void setCirculationPumpState(eOutput in) void setCirculationPumpState(eOutput in)
@ -110,7 +70,6 @@ void setCirculationPumpState(eOutput in)
break; break;
case DISABLED: case DISABLED:
gpio_set_level(uCirculationPumpGpioPin, 1U); // Switch off Circulation Pump gpio_set_level(uCirculationPumpGpioPin, 1U); // Switch off Circulation Pump
break;
default: default:
break; break;
} }
@ -149,7 +108,6 @@ void setBurnerState(eOutput in)
break; break;
case DISABLED: case DISABLED:
gpio_set_level(uBurnerGpioPin, 1U); // Switch off Burner gpio_set_level(uBurnerGpioPin, 1U); // Switch off Burner
break;
default: default:
break; break;
} }
@ -188,7 +146,6 @@ void setSafetyControlState(eOutput in)
break; break;
case DISABLED: case DISABLED:
gpio_set_level(uSafetyContactGpioPin, 1U); // Switch off power for Burner gpio_set_level(uSafetyContactGpioPin, 1U); // Switch off power for Burner
break;
default: default:
break; break;
} }

View File

@ -1,70 +1,15 @@
/**
* @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, /**< Output active (relay energized, GPIO low). */ ENABLED,
DISABLED /**< Output inactive (relay de-energized, GPIO high). */ DISABLED
} 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,79 +1,56 @@
/**
* @file safety.c
* @brief Implementation of safety monitoring module.
*/
#include "safety.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "esp_log.h" #include "esp_log.h"
#include <string.h> #include <string.h>
#include <math.h> #include "safety.h"
/** @brief Task interval in seconds. */ #define PERIODIC_INTERVAL 1U // run safety checks every 1sec
#define PERIODIC_INTERVAL 1U #define SENSOR_GRACE_PERIOD (60U * 30U) // period that a sensor can report the same reading in seconds
/** @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 = "safety";
static const char *TAG = "smart-oil-heater-control-system-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", {95.0f, -10.0f}, 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", {45.0f, -20.0f}, 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", {95.0f, -10.0f}, 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", {95.0f, -10.0f}, 0.0f, 0U, getReturnFlowTemperature}};
static eSafetyState sSafetyState = SAFETY_NO_ERROR; static eSafetyState sSafetyState = SAFETY_NO_ERROR;
/* Private function prototypes */ void taskSafety(void *pvParameters);
static void taskSafety(void *pvParameters); void checkSensorSanity(void);
static void checkSensorSanity(void); void setSafeState(void);
static void setSafeState(void);
esp_err_t initSafety(void) void initSafety(void)
{ {
xMutexAccessSafety = xSemaphoreCreateRecursiveMutex(); xMutexAccessSafety = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessSafety == NULL) if (xMutexAccessSafety == NULL)
{ {
ESP_LOGE(TAG, "Failed to create mutex"); ESP_LOGE(TAG, "Unable to create mutex");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessSafety); xSemaphoreGiveRecursive(xMutexAccessSafety);
BaseType_t taskCreated = xTaskCreate( BaseType_t taskCreated = xTaskCreate(
taskSafety, taskSafety, // Function to implement the task
"taskSafety", "taskSafety", // Task name
4096, 4096, // Stack size (in words, not bytes)
NULL, NULL, // Parameters to the task function (none in this case)
5, 5, // Task priority (higher number = higher priority)
NULL); NULL // Task handle (optional)
);
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(); setSafeState(); // Set inital state
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)
{ {
@ -81,6 +58,7 @@ static 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)
@ -93,10 +71,7 @@ static 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++)
@ -116,7 +91,7 @@ static void checkSensorSanity(void)
} }
else else
{ {
if (fabsf(sCurrentMeasurement.fCurrentValue - sanityChecks[i].fSensorTemperatureLast) < FLOAT_EPSILON) if (sCurrentMeasurement.fCurrentValue == sanityChecks[i].fSensorTemperatureLast)
{ {
sanityChecks[i].uUnchangedCounter++; sanityChecks[i].uUnchangedCounter++;
if (sanityChecks[i].uUnchangedCounter >= (SENSOR_GRACE_PERIOD / PERIODIC_INTERVAL)) if (sanityChecks[i].uUnchangedCounter >= (SENSOR_GRACE_PERIOD / PERIODIC_INTERVAL))
@ -128,7 +103,6 @@ static void checkSensorSanity(void)
} }
else else
{ {
sanityChecks[i].uUnchangedCounter = 0U;
sanityChecks[i].fSensorTemperatureLast = sCurrentMeasurement.fCurrentValue; sanityChecks[i].fSensorTemperatureLast = sCurrentMeasurement.fCurrentValue;
if (sCurrentMeasurement.fCurrentValue > sanityChecks[i].sSensorLimit.max) if (sCurrentMeasurement.fCurrentValue > sanityChecks[i].sSensorLimit.max)
@ -145,17 +119,16 @@ static void checkSensorSanity(void)
} }
else else
{ {
sanityChecks[i].uUnchangedCounter = 0U;
sanityChecks[i].state = SENSOR_NO_ERROR; sanityChecks[i].state = SENSOR_NO_ERROR;
} }
} }
} }
// printf(" state: %u\n", sanityChecks[i].state);
} }
} }
/** 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
@ -170,7 +143,7 @@ void getSensorSanityStates(sSensorSanityCheck *pSensorSanityChecks)
{ {
// Copy only the needed attributes // Copy only the needed attributes
pSensorSanityChecks[i].state = sanityChecks[i].state; pSensorSanityChecks[i].state = sanityChecks[i].state;
strncpy(pSensorSanityChecks[i].name, sanityChecks[i].name, MAX_ERROR_STRING_SIZE); strcpy(pSensorSanityChecks[i].name, sanityChecks[i].name);
} }
xSemaphoreGiveRecursive(xMutexAccessSafety); xSemaphoreGiveRecursive(xMutexAccessSafety);
} }

View File

@ -1,117 +1,43 @@
/**
* @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 "esp_err.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)
/** @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 typedef enum _SensorErrorState
{ {
SENSOR_NO_ERROR, /**< Sensor operating normally. */ SENSOR_NO_ERROR,
SENSOR_TOO_HIGH, /**< Temperature above maximum limit. */ SENSOR_TOO_HIGH,
SENSOR_TOO_LOW, /**< Temperature below minimum limit. */ SENSOR_TOO_LOW,
SENSOR_UNCHANGED, /**< Temperature unchanged for too long. */ SENSOR_UNCHANGED,
SENSOR_NOT_FOUND /**< Sensor not responding. */ SENSOR_NOT_FOUND
} eSensorErrorState; } eSensorErrorState;
/**
* @brief Overall safety state enumeration.
*/
typedef enum _SafetyState typedef enum _SafetyState
{ {
SAFETY_NO_ERROR, /**< All sensors OK. */ SAFETY_NO_ERROR,
SAFETY_SENSOR_ERROR, /**< At least one sensor failed. */ SAFETY_SENSOR_ERROR,
SAFETY_INTERNAL_ERROR /**< Internal module error. */ SAFETY_INTERNAL_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; /**< Current error state. */ eSensorErrorState state;
char name[MAX_ERROR_STRING_SIZE]; /**< Sensor name for logging. */ char name[MAX_ERROR_STRING_SIZE];
sTemperatureSensorLimit sSensorLimit; /**< Temperature limits. */ sTemperatureSensorLimit sSensorLimit;
float fSensorTemperatureLast; /**< Last temperature reading. */ float fSensorTemperatureLast;
uint32_t uUnchangedCounter; /**< Counter for unchanged readings. */ uint32_t uUnchangedCounter;
GetSensorValue getSensor; /**< Function to get sensor value. */ GetSensorValue getSensor;
} 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,32 +1,21 @@
/** #include <time.h>
* @file sntp.c #include <sys/time.h>
* @brief Implementation of SNTP client module. #include <esp_sntp.h>
*/ #include "esp_log.h"
#include "sntp.h" #include "sntp.h"
#include "esp_sntp.h" static const char *TAG = "smart-oil-heater-control-system-sntp";
#include "esp_log.h" static eSntpState sntpState = SYNC_NOT_STARTED;
void time_sync_notification_cb(struct timeval *tv);
#include <time.h> void initSntp(void)
#include <sys/time.h>
static const char *TAG = "sntp";
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)
@ -34,12 +23,8 @@ 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, "Time synchronized! Unix Time: %lld", tv->tv_sec); ESP_LOGI(TAG, "SNTP synchronization! Unix Time: %lld", tv->tv_sec);
sntpState = SYNC_SUCCESSFUL; sntpState = SYNC_SUCCESSFUL;
} }

View File

@ -1,37 +1,11 @@
/**
* @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, /**< Time synchronized successfully. */ SYNC_SUCCESSFUL,
SYNC_NOT_STARTED, /**< Synchronization not yet attempted. */ SYNC_NOT_STARTED,
SYNC_FAILED, /**< Synchronization failed. */ SYNC_FAILED,
} eSntpState; } eSntpState;
/** void initSntp(void);
* @brief Initialize the SNTP client. eSntpState getSntpState(void);
*
* 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,62 +1,39 @@
/** #include <string.h>
* @file wifi.c
* @brief Implementation of WiFi station mode module.
*/
#include "wifi.h"
#include "esp_timer.h" #include "esp_timer.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h" #include "esp_wifi.h"
#include "esp_event.h" #include "esp_event.h"
#include "freertos/event_groups.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_netif.h" #include "esp_netif.h"
#include <lwip/sockets.h> #include <lwip/sockets.h>
#include <string.h> #include "wifi.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. */ static const char *TAG = "smart-oil-heater-control-system-wifi";
#define MAX_RETRY_COUNT 10
/** @brief Delay between retries in milliseconds. */
#define RETRY_DELAY_MS 1000
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 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);
static bool parse_bssid(const char *bssid_str, uint8_t *bssid);
esp_err_t initWifi(void) void initWifi(void)
{ {
s_wifi_event_group = xEventGroupCreate(); s_wifi_event_group = xEventGroupCreate();
if (s_wifi_event_group == NULL)
{
ESP_LOGE(TAG, "Failed to create event group");
return ESP_FAIL;
}
ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default()); ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *my_sta = esp_netif_create_default_wifi_sta(); esp_netif_t *my_sta = esp_netif_create_default_wifi_sta();
ESP_ERROR_CHECK(esp_netif_dhcpc_stop(my_sta)); esp_netif_dhcpc_stop(my_sta);
esp_netif_ip_info_t ip_info; esp_netif_ip_info_t ip_info;
ip_info.ip.addr = ipaddr_addr(CONFIG_STATIC_IP_ADDR); ip_info.ip.addr = ipaddr_addr(CONFIG_STATIC_IP_ADDR);
ip_info.gw.addr = ipaddr_addr(CONFIG_STATIC_GATEWAY_IP_ADDR); ip_info.gw.addr = ipaddr_addr(CONFIG_STATIC_GATEWAY_IP_ADDR);
ip_info.netmask.addr = ipaddr_addr(CONFIG_STATIC_IP_NETMASK); ip_info.netmask.addr = ipaddr_addr(CONFIG_STATIC_IP_NETMASK);
ESP_ERROR_CHECK(esp_netif_set_ip_info(my_sta, &ip_info)); esp_netif_set_ip_info(my_sta, &ip_info);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_init(&cfg));
@ -80,15 +57,30 @@ esp_err_t initWifi(void)
.threshold.authmode = WIFI_AUTH_WPA2_PSK, .threshold.authmode = WIFI_AUTH_WPA2_PSK,
}, },
}; };
#if CONFIG_ENV_WIFI_BSSID_LOCK
/* Lock to specific AP by BSSID */
if (parse_bssid(CONFIG_ENV_WIFI_BSSID, wifi_config.sta.bssid))
{
wifi_config.sta.bssid_set = true;
ESP_LOGI(TAG, "BSSID lock enabled: %s", CONFIG_ENV_WIFI_BSSID);
}
else
{
ESP_LOGE(TAG, "Invalid BSSID format: %s", CONFIG_ENV_WIFI_BSSID);
wifi_config.sta.bssid_set = false;
}
#endif
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(78)); 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)); ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); // Use power-saving mode
ESP_LOGI(TAG, "WiFi init finished, waiting for connection..."); ESP_LOGI(TAG, "wifi_init_sta finished.");
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,
@ -98,32 +90,19 @@ esp_err_t 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_LOGE(TAG, "Failed to connect to SSID:%s", CONFIG_SSID); ESP_LOGI(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;
} }
vEventGroupDelete(s_wifi_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, 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)
{ {
@ -133,46 +112,37 @@ static void event_handler(void *arg, esp_event_base_t event_base,
} }
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{ {
wifi_event_sta_disconnected_t *event = (wifi_event_sta_disconnected_t *)event_data; esp_wifi_connect();
ESP_LOGW(TAG, "Disconnected from AP (reason: %d)", event->reason); ESP_LOGI(TAG, "Retry to connect to the AP");
if (s_initial_connect)
{
// During initial connection phase, use retry limit
if (s_retry_num < MAX_RETRY_COUNT)
{
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(TAG, "Retry to connect to the AP (%d/%d)", s_retry_num, MAX_RETRY_COUNT);
}
else
{
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
ESP_LOGE(TAG, "Failed to connect after %d attempts", MAX_RETRY_COUNT);
}
}
else
{
// After initial connection, always try to reconnect with delay
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
esp_wifi_connect();
ESP_LOGI(TAG, "Attempting to reconnect to the AP...");
}
} }
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{ {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip)); ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
if (s_initial_connect)
{
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
else
{
ESP_LOGI(TAG, "Successfully reconnected to AP");
}
} }
} }
/**
* @brief Parse BSSID string to byte array
*
* @param bssid_str BSSID string in format "XX:XX:XX:XX:XX:XX"
* @param bssid Output byte array (6 bytes)
* @return true on success, false on parse error
*/
static bool parse_bssid(const char *bssid_str, uint8_t *bssid)
{
unsigned int tmp[6];
int parsed = sscanf(bssid_str, "%x:%x:%x:%x:%x:%x",
&tmp[0], &tmp[1], &tmp[2],
&tmp[3], &tmp[4], &tmp[5]);
if (parsed != 6)
{
return false;
}
for (int i = 0; i < 6; i++)
{
bssid[i] = (uint8_t)tmp[i];
}
return true;
}

View File

@ -1,22 +1,3 @@
/**
* @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
#include "esp_err.h" void initWifi(void);
/**
* @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);