22 Commits

Author SHA256 Message Date
4f355bdfdf update README 2026-01-10 18:51:33 +01:00
430b4cb690 cleanup README 2026-01-10 13:42:24 +01:00
1d4e272d80 error handling and cleanup 2026-01-10 13:32:49 +01:00
f8f6af53bd implement config 2026-01-10 12:50:06 +01:00
f3f6f1bc5f Potential division by zero 2026-01-10 12:01:22 +01:00
b718073907 Missing break before default 2026-01-10 11:58:46 +01:00
d36b91a0fd Variable name shadows type name 2026-01-10 11:57:15 +01:00
40f757b7d1 uUnchangedCounter reset logic flaw 2026-01-10 11:54:18 +01:00
a9ec101bc6 Floating-point equality comparison 2026-01-10 11:52:08 +01:00
0236ebcdd1 Unsafe strcpy 2026-01-10 11:47:04 +01:00
05757a5038 Unchecked WiFi API call 2026-01-10 11:45:49 +01:00
020eb63e05 Unchecked network configuration 2026-01-10 11:43:26 +01:00
67929580d5 Unchecked xEventGroupCreate 2026-01-10 11:42:27 +01:00
10f9645580 Unchecked gpio_config returns 2026-01-10 11:39:37 +01:00
df3825df3a Non-thread-safe function 2026-01-10 11:33:37 +01:00
8c3dbc2886 Unprotected shared state access 2026-01-10 11:31:34 +01:00
267197ec20 Missing mutex protection 2026-01-10 11:06:10 +01:00
781f9a1445 ncorrect memset with strlen 2026-01-10 11:02:31 +01:00
09a3c3a22d Misuse of ESP_ERROR_CHECK 2026-01-10 10:58:12 +01:00
0775fda0ca Off-by-one error (buffer overread) 2026-01-10 10:55:15 +01:00
cd73985740 Wrong memset size 2026-01-10 10:54:32 +01:00
af307fd403 handle reconnect 2026-01-09 23:35:44 +01:00
17 changed files with 1652 additions and 574 deletions

168
README.md
View File

@ -1,83 +1,41 @@
# smart-oil-heating-control-system
# Smart Oil Heating Control System
## Software
### Design
ESP32-based control system for oil-fired central heating with schedule-based temperature management, safety monitoring, and Prometheus metrics export.
## Features
- **Schedule Control**: Day/night temperature targets per weekday
- **Summer Mode**: Automatic heating disable based on outdoor temperature
- **Safety Monitoring**: Sensor sanity checks with automatic safe-state fallback
- **Prometheus Metrics**: HTTP endpoint at port 9100
## System Overview
```mermaid
classDiagram
Inputs <|-- Control
Outputs <|-- Control
Sntp <|-- Control
Inputs <|-- Safety
Outputs <|--|> Safety
flowchart TB
subgraph OUTSIDE[" "]
OT[/"🌡️ Outdoor Temp<br/>DS18B20"/]
end
Inputs <|-- Metrics
Outputs <|-- Metrics
Control <|-- Metrics
Safety <|-- Metrics
Sntp <|-- Metrics
subgraph BURNER["OIL BURNER"]
CT[/"🌡️ Chamber Temp<br/>DS18B20"/]
BF[["⚠️ Burner Fault<br/>GPIO19 INPUT"]]
BR(["🔥 Burner Relay<br/>GPIO14"])
SC(["🔌 Safety Contact<br/>GPIO12"])
end
class Inputs{
+initInputs()
-initMeasurement()
-updateAverage()
-updatePrediction()
-taskInput()
-linearRegressionPredict()
+getChamberTemperature()
+getOutdoorTemperature()
+getInletFlowTemperature()
+getReturnFlowTemperature()
+getBurnerError()
}
subgraph CIRCUIT["HEATING CIRCUIT"]
IT[/"🌡️ Inlet Temp<br/>DS18B20"/]
CP(["💧 Circulation Pump<br/>GPIO27"])
RT[/"🌡️ Return Temp<br/>DS18B20"/]
end
class Outputs{
+initOutputs()
+getCirculationPumpState()
+setCirculationPumpState()
+getBurnerState()
+setBurnerState()
+getSafetyControlState()
+setSafetyControlState()
}
RAD["🏠 Radiators"]
class Control{
initControl()
+taskControl()
+getControlCurrentWeekday()
-findControlCurrentTemperatureEntry()
+getControlCurrentTemperatureEntry()
-controlTable
+getControlState()
}
class Safety{
+initSafety()
-taskSafety()
-setSafeState()
-checkSensorSanity()
+getSensorSanityStates()
+getSafetyState()
}
class Wifi{
+initWifi()
}
class Sntp{
+initSntp()
+getSntpState()
}
class Metrics{
+initMetrics()
-taskMetrics()
-metrics
+event_handler()
+connect_wifi()
+setMetrics()
}
BURNER -->|"hot water"| IT
IT --> CP
CP --> RAD
RAD --> RT
RT -->|"cold water"| BURNER
```
### Prometheus Metrics
@ -88,26 +46,26 @@ burner_fault_pending 1
circulation_pump_enabled 1
burner_enabled 0
safety_contact_enabled 1
chamber_temperature 37.250000
chamber_temperature_avg10 37.237499
chamber_temperature_avg60 37.438541
chamber_temperature_damped 42.185040
chamber_temperature_pred60 36.638443
inlet_flow_temperature 35.625000
inlet_flow_temperature_avg10 35.618752
inlet_flow_temperature_avg60 35.415627
inlet_flow_temperature_damped 39.431259
inlet_flow_temperature_pred60 36.078678
outdoor_temperature 14.687500
outdoor_temperature_avg10 14.662500
outdoor_temperature_avg60 14.646875
outdoor_temperature_damped 9.169084
outdoor_temperature_pred60 14.660233
return_flow_temperature 39.937500
return_flow_temperature_avg10 40.087502
return_flow_temperature_avg60 41.146873
return_flow_temperature_damped 32.385151
return_flow_temperature_pred60 37.311958
chamber_temperature 37.312500
chamber_temperature_avg10 37.393749
chamber_temperature_avg60 37.689583
chamber_temperature_damped 38.058098
chamber_temperature_pred60 36.697266
inlet_flow_temperature 34.562500
inlet_flow_temperature_avg10 34.587502
inlet_flow_temperature_avg60 34.880207
inlet_flow_temperature_damped 35.255993
inlet_flow_temperature_pred60 33.910374
outdoor_temperature 1.812500
outdoor_temperature_avg10 1.825000
outdoor_temperature_avg60 1.821875
outdoor_temperature_damped 2.390663
outdoor_temperature_pred60 1.840263
return_flow_temperature 34.125000
return_flow_temperature_avg10 34.162498
return_flow_temperature_avg60 34.304165
return_flow_temperature_damped 31.430506
return_flow_temperature_pred60 33.858772
chamber_temperature_state 0
outdoor_temperature_state 0
inlet_flow_temperature_state 0
@ -115,13 +73,13 @@ return_flow_temperature_state 0
safety_state 0
control_state 3
control_current_weekday 5
control_current_entry_time 17100
control_current_entry_time 24300
control_current_entry_chamber_temperature 80.000000
control_current_entry_return_flow_temperature 30.000000
sntp_state 0
system_unixtime 1762012743
uptime_seconds 465229
wifi_rssi -72
system_unixtime 1768067412
uptime_seconds 344878
wifi_rssi -59
```
#### Status Encoding
@ -180,4 +138,20 @@ wifi_rssi -72
| Input Burner Fault | IO19 | Digital Input IN1 |
| Input Temperature DS10B20 | IO04 | 1-Wire |
## Configuration
All parameters configurable via `idf.py menuconfig`:
- WiFi credentials and static IP
- GPIO pin assignments
- 1-Wire sensor addresses
- Temperature thresholds and limits
- Heating schedule (day/night per weekday)
- Damping factors
## Building
```bash
idf.py set-target esp32
idf.py menuconfig # Configure settings
idf.py build flash monitor
```

View File

@ -1,22 +1,391 @@
menu "Smart Oil Heating Control System"
config SSID
string "SSID"
default "my WiFi SSID"
config WIFI_PASSWORD
string "WIFI_PASSWORD"
default "my WIFI Password"
config STATIC_IP_ADDR
string "Static IPv4 address"
default "192.168.0.42"
config STATIC_IP_NETMASK
string "Static IPv4 netmask"
default "255.255.0.0"
config STATIC_GATEWAY_IP_ADDR
string "Static IPv4 gateway address"
default "192.168.0.1"
config SNTP_SERVER_IP_ADDR
string "SNTP IPv4 server address"
default "192.168.0.1"
menu "WiFi Configuration"
config SSID
string "WiFi SSID"
default "my WiFi SSID"
help
The SSID of the WiFi network to connect to.
config WIFI_PASSWORD
string "WiFi Password"
default "my WIFI Password"
help
The password for the WiFi network.
config STATIC_IP_ADDR
string "Static IPv4 address"
default "192.168.0.42"
help
Static IP address for the ESP32.
config STATIC_IP_NETMASK
string "Static IPv4 netmask"
default "255.255.0.0"
help
Network mask for the static IP configuration.
config STATIC_GATEWAY_IP_ADDR
string "Static IPv4 gateway address"
default "192.168.0.1"
help
Gateway IP address for network routing.
config SNTP_SERVER_IP_ADDR
string "SNTP server address"
default "192.168.0.1"
help
NTP server address for time synchronization.
endmenu
menu "GPIO Configuration"
menu "Input GPIOs"
config GPIO_BURNER_FAULT
int "Burner fault input GPIO"
range 0 39
default 19
help
GPIO pin connected to the burner fault signal.
config GPIO_DS18B20_ONEWIRE
int "DS18B20 1-Wire bus GPIO"
range 0 39
default 4
help
GPIO pin for the 1-Wire bus (DS18B20 temperature sensors).
endmenu
menu "Output GPIOs"
config GPIO_CIRCULATION_PUMP
int "Circulation pump output GPIO"
range 0 39
default 27
help
GPIO pin to control the circulation pump relay.
config GPIO_BURNER
int "Burner control output GPIO"
range 0 39
default 14
help
GPIO pin to control the burner relay.
config GPIO_SAFETY_CONTACT
int "Safety contact output GPIO"
range 0 39
default 12
help
GPIO pin for the safety contact relay (main power to burner).
endmenu
endmenu
menu "1-Wire Sensor Addresses"
config ONEWIRE_ADDR_CHAMBER_TEMP
hex "Chamber temperature sensor address"
default 0xd00000108cd01d28
help
64-bit 1-Wire address of the chamber temperature sensor.
config ONEWIRE_ADDR_OUTDOOR_TEMP
hex "Outdoor temperature sensor address"
default 0xd70000108a9b9128
help
64-bit 1-Wire address of the outdoor temperature sensor.
config ONEWIRE_ADDR_INLET_FLOW_TEMP
hex "Inlet flow temperature sensor address"
default 0x410000108b8c0628
help
64-bit 1-Wire address of the inlet flow temperature sensor.
config ONEWIRE_ADDR_RETURN_FLOW_TEMP
hex "Return flow temperature sensor address"
default 0x90000108cc77c28
help
64-bit 1-Wire address of the return flow temperature sensor.
endmenu
menu "Temperature Control Settings"
menu "Target Temperatures"
config TEMP_RETURN_FLOW_LOWER_LIMIT_DAY
int "Return flow lower limit (day) [°C x 10]"
range 150 500
default 300
help
Minimum return flow temperature during day mode in 0.1°C units.
Example: 300 = 30.0°C
config TEMP_RETURN_FLOW_LOWER_LIMIT_NIGHT
int "Return flow lower limit (night) [°C x 10]"
range 150 500
default 250
help
Minimum return flow temperature during night mode in 0.1°C units.
Example: 250 = 25.0°C
config TEMP_CHAMBER_TARGET
int "Chamber target temperature [°C x 10]"
range 500 950
default 800
help
Maximum chamber temperature target in 0.1°C units.
Example: 800 = 80.0°C
config TEMP_CHAMBER_THRESHOLD
int "Chamber temperature threshold [°C x 10]"
range 300 700
default 450
help
Minimum chamber temperature to enable burner in 0.1°C units.
Example: 450 = 45.0°C
config TEMP_CIRCULATION_PUMP_THRESHOLD
int "Circulation pump threshold [°C x 10]"
range 200 500
default 300
help
Minimum chamber temperature to enable circulation pump in 0.1°C units.
Example: 300 = 30.0°C
endmenu
menu "Summer Mode Settings"
config TEMP_SUMMER_MODE_HIGH
int "Summer mode activation threshold [°C x 10]"
range 150 300
default 200
help
Outdoor temperature above which summer mode activates in 0.1°C units.
Example: 200 = 20.0°C
config TEMP_SUMMER_MODE_LOW
int "Summer mode deactivation threshold [°C x 10]"
range 100 250
default 150
help
Outdoor temperature below which summer mode deactivates in 0.1°C units.
Example: 150 = 15.0°C
endmenu
config BURNER_FAULT_DETECTION_SECONDS
int "Burner fault detection timeout (seconds)"
range 60 600
default 240
help
Time in seconds to wait before checking for burner fault after enabling.
endmenu
menu "Sensor Limits"
menu "Chamber Temperature Limits"
config SENSOR_LIMIT_CHAMBER_MAX
int "Chamber sensor maximum [°C x 10]"
range 500 1200
default 950
help
Maximum valid chamber temperature reading in 0.1°C units.
config SENSOR_LIMIT_CHAMBER_MIN
int "Chamber sensor minimum [°C x 10]"
range -400 100
default -100
help
Minimum valid chamber temperature reading in 0.1°C units.
endmenu
menu "Outdoor Temperature Limits"
config SENSOR_LIMIT_OUTDOOR_MAX
int "Outdoor sensor maximum [°C x 10]"
range 300 600
default 450
help
Maximum valid outdoor temperature reading in 0.1°C units.
config SENSOR_LIMIT_OUTDOOR_MIN
int "Outdoor sensor minimum [°C x 10]"
range -500 0
default -200
help
Minimum valid outdoor temperature reading in 0.1°C units.
endmenu
menu "Inlet Flow Temperature Limits"
config SENSOR_LIMIT_INLET_MAX
int "Inlet flow sensor maximum [°C x 10]"
range 500 1200
default 950
help
Maximum valid inlet flow temperature reading in 0.1°C units.
config SENSOR_LIMIT_INLET_MIN
int "Inlet flow sensor minimum [°C x 10]"
range -400 100
default -100
help
Minimum valid inlet flow temperature reading in 0.1°C units.
endmenu
menu "Return Flow Temperature Limits"
config SENSOR_LIMIT_RETURN_MAX
int "Return flow sensor maximum [°C x 10]"
range 500 1200
default 950
help
Maximum valid return flow temperature reading in 0.1°C units.
config SENSOR_LIMIT_RETURN_MIN
int "Return flow sensor minimum [°C x 10]"
range -400 100
default -100
help
Minimum valid return flow temperature reading in 0.1°C units.
endmenu
config SENSOR_GRACE_PERIOD_MINUTES
int "Sensor unchanged grace period (minutes)"
range 1 120
default 30
help
Maximum time in minutes a sensor can report unchanged values
before being flagged as faulty.
endmenu
menu "Damping Factors"
config DAMPING_FACTOR_WARMER
int "Damping factor warmer [x 0.00001]"
range 1 100
default 1
help
Damping factor for rising temperatures in units of 0.00001.
Example: 1 = 0.00001 (0.001%)
config DAMPING_FACTOR_COLDER
int "Damping factor colder [x 0.00001]"
range 1 100
default 5
help
Damping factor for falling temperatures in units of 0.00001.
Example: 5 = 0.00005 (0.005%)
endmenu
menu "Heating Schedule"
menu "Weekday Schedule (Monday-Thursday)"
config SCHEDULE_WEEKDAY_DAY_START_HOUR
int "Day mode start hour"
range 0 23
default 4
help
Hour when day mode starts on weekdays (24h format).
config SCHEDULE_WEEKDAY_DAY_START_MINUTE
int "Day mode start minute"
range 0 59
default 45
help
Minute when day mode starts on weekdays.
config SCHEDULE_WEEKDAY_NIGHT_START_HOUR
int "Night mode start hour"
range 0 23
default 22
help
Hour when night mode starts on weekdays (24h format).
config SCHEDULE_WEEKDAY_NIGHT_START_MINUTE
int "Night mode start minute"
range 0 59
default 0
help
Minute when night mode starts on weekdays.
endmenu
menu "Friday Schedule"
config SCHEDULE_FRIDAY_DAY_START_HOUR
int "Day mode start hour"
range 0 23
default 4
help
Hour when day mode starts on Friday (24h format).
config SCHEDULE_FRIDAY_DAY_START_MINUTE
int "Day mode start minute"
range 0 59
default 45
help
Minute when day mode starts on Friday.
config SCHEDULE_FRIDAY_NIGHT_START_HOUR
int "Night mode start hour"
range 0 23
default 23
help
Hour when night mode starts on Friday (24h format).
config SCHEDULE_FRIDAY_NIGHT_START_MINUTE
int "Night mode start minute"
range 0 59
default 0
help
Minute when night mode starts on Friday.
endmenu
menu "Saturday Schedule"
config SCHEDULE_SATURDAY_DAY_START_HOUR
int "Day mode start hour"
range 0 23
default 6
help
Hour when day mode starts on Saturday (24h format).
config SCHEDULE_SATURDAY_DAY_START_MINUTE
int "Day mode start minute"
range 0 59
default 45
help
Minute when day mode starts on Saturday.
config SCHEDULE_SATURDAY_NIGHT_START_HOUR
int "Night mode start hour"
range 0 23
default 23
help
Hour when night mode starts on Saturday (24h format).
config SCHEDULE_SATURDAY_NIGHT_START_MINUTE
int "Night mode start minute"
range 0 59
default 30
help
Minute when night mode starts on Saturday.
endmenu
menu "Sunday Schedule"
config SCHEDULE_SUNDAY_DAY_START_HOUR
int "Day mode start hour"
range 0 23
default 6
help
Hour when day mode starts on Sunday (24h format).
config SCHEDULE_SUNDAY_DAY_START_MINUTE
int "Day mode start minute"
range 0 59
default 45
help
Minute when day mode starts on Sunday.
config SCHEDULE_SUNDAY_NIGHT_START_HOUR
int "Night mode start hour"
range 0 23
default 22
help
Hour when night mode starts on Sunday (24h format).
config SCHEDULE_SUNDAY_NIGHT_START_MINUTE
int "Night mode start minute"
range 0 59
default 30
help
Minute when night mode starts on Sunday.
endmenu
endmenu
endmenu

View File

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

View File

@ -1,26 +1,74 @@
#pragma once
#include <time.h>
/**
* @file control.h
* @brief Heating control logic with schedule-based temperature management.
*
* This module implements the main heating control loop. It manages
* burner operation based on time schedules, temperature targets,
* and summer mode detection.
*/
#pragma once
#include "sdkconfig.h"
#include "esp_err.h"
#include <stdint.h>
#include <stddef.h>
/** @brief Maximum number of temperature entries per day. */
#define MAX_TEMPERATURE_ENTRIES_PER_DAY 24U
/** @brief Return flow target temperature for day mode (°C). */
#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY (CONFIG_TEMP_RETURN_FLOW_LOWER_LIMIT_DAY / 10.0f)
/** @brief Return flow target temperature for night mode (°C). */
#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT (CONFIG_TEMP_RETURN_FLOW_LOWER_LIMIT_NIGHT / 10.0f)
/** @brief Chamber target temperature (°C). */
#define CHAMBER_TEMPERATURE_TARGET (CONFIG_TEMP_CHAMBER_TARGET / 10.0f)
/** @brief Chamber temperature threshold to enable burner (°C). */
#define CHAMBER_TEMPERATURE_THRESHOLD (CONFIG_TEMP_CHAMBER_THRESHOLD / 10.0f)
/** @brief Outdoor temperature to activate summer mode (°C). */
#define SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH (CONFIG_TEMP_SUMMER_MODE_HIGH / 10.0f)
/** @brief Outdoor temperature to deactivate summer mode (°C). */
#define SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW (CONFIG_TEMP_SUMMER_MODE_LOW / 10.0f)
/** @brief Chamber temperature threshold for circulation pump (°C). */
#define CIRCULATION_PUMP_TEMPERATURE_THRESHOLD (CONFIG_TEMP_CIRCULATION_PUMP_THRESHOLD / 10.0f)
/** @brief Time to wait before checking burner fault (seconds). */
#define BURNER_FAULT_DETECTION_THRESHOLD CONFIG_BURNER_FAULT_DETECTION_SECONDS
/**
* @brief Control state enumeration.
*/
typedef enum _ControlState
{
CONTROL_STARTING,
CONTROL_HEATING,
CONTROL_OUTDOOR_TOO_WARM,
CONTROL_RETURN_FLOW_TOO_WARM,
CONTROL_FAULT_BURNER,
CONTROL_FAULT_SAFETY,
CONTROL_FAULT_SNTP,
CONTROL_STARTING, /**< System starting up. */
CONTROL_HEATING, /**< Burner running. */
CONTROL_OUTDOOR_TOO_WARM, /**< Summer mode active. */
CONTROL_RETURN_FLOW_TOO_WARM, /**< Target temperature reached. */
CONTROL_FAULT_BURNER, /**< Burner fault detected. */
CONTROL_FAULT_SAFETY, /**< Safety fault detected. */
CONTROL_FAULT_SNTP, /**< SNTP sync failed. */
} eControlState;
/**
* @brief Burner operational state enumeration.
*/
typedef enum _BurnerState
{
BURNER_UNKNOWN, // Burner is disabled or state after enabling is still unkown
BURNER_FIRED, // Burner fired successfully
BURNER_FAULT // Burner was unable to fire successfully
BURNER_UNKNOWN, /**< Burner state unknown after enable. */
BURNER_FIRED, /**< Burner fired successfully. */
BURNER_FAULT /**< Burner failed to fire. */
} eBurnerState;
/**
* @brief Weekday enumeration (Monday = 0).
*/
typedef enum _ControlWeekday
{
MONDAY,
@ -32,27 +80,58 @@ typedef enum _ControlWeekday
SUNDAY,
} eControlWeekday;
/**
* @brief Time of day structure.
*/
typedef struct _ControlTimestamp
{
uint8_t hour;
uint8_t minute;
uint8_t hour; /**< Hour (0-23). */
uint8_t minute; /**< Minute (0-59). */
} sControlTimestamp;
/**
* @brief Temperature schedule entry.
*/
typedef struct _ControlTemperatureEntry
{
sControlTimestamp timestamp;
float fReturnFlowTemperature;
float fChamberTemperature;
sControlTimestamp timestamp; /**< Time when entry becomes active. */
float fReturnFlowTemperature; /**< Target return flow temperature. */
float fChamberTemperature; /**< Target chamber temperature. */
} sControlTemperatureEntry;
/**
* @brief Daily schedule structure.
*/
typedef struct _ControlDay
{
eControlWeekday day;
size_t entryCount; // number of entries for each day
sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY];
eControlWeekday day; /**< Day of week. */
size_t entryCount; /**< Number of entries for this day. */
sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY]; /**< Schedule entries. */
} sControlDay;
void initControl(void);
/**
* @brief Initialize the control module.
*
* Creates the control task that manages heating operation.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initControl(void);
/**
* @brief Get the current control state.
* @return eControlState indicating current operation mode.
*/
eControlState getControlState(void);
/**
* @brief Get the current weekday.
* @return eControlWeekday (Monday = 0, Sunday = 6).
*/
eControlWeekday getControlCurrentWeekday(void);
/**
* @brief Get the currently active temperature entry.
* @return sControlTemperatureEntry with current targets.
*/
sControlTemperatureEntry getControlCurrentTemperatureEntry(void);

View File

@ -1,29 +1,51 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include <string.h>
#include <math.h>
#include "esp_log.h"
#include <ds18x20.h>
/**
* @file inputs.c
* @brief Implementation of input handling module.
*/
#include "inputs.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include <ds18x20.h>
#include <string.h>
#include <math.h>
/** @brief Maximum number of DS18B20 sensors supported. */
#define MAX_DN18B20_SENSORS 4U
#define ONE_WIRE_LOOPS 4U // try to read the 1-Wire sensors that often
#define PERIODIC_INTERVAL 1U // read and compute the inputs every 1sec
static const char *TAG = "smart-oil-heater-control-system-inputs";
const uint8_t uBurnerFaultPin = 19U;
const uint8_t uDS18B20Pin = 4U;
/** @brief Number of retry attempts for 1-Wire read. */
#define ONE_WIRE_LOOPS 4U
const onewire_addr_t uChamperTempSensorAddr = 0xd00000108cd01d28;
const onewire_addr_t uOutdoorTempSensorAddr = 0xd70000108a9b9128;
const onewire_addr_t uInletFlowTempSensorAddr = 0x410000108b8c0628;
const onewire_addr_t uReturnFlowTempSensorAddr = 0x90000108cc77c28;
/** @brief Task interval in seconds. */
#define PERIODIC_INTERVAL 1U
onewire_addr_t uOneWireAddresses[MAX_DN18B20_SENSORS];
float fDS18B20Temps[MAX_DN18B20_SENSORS];
size_t sSensorCount = 0U;
static const char *TAG = "inputs";
/** @brief Burner fault GPIO pin (from Kconfig). */
static const uint8_t uBurnerFaultPin = CONFIG_GPIO_BURNER_FAULT;
/** @brief DS18B20 1-Wire GPIO pin (from Kconfig). */
static const uint8_t uDS18B20Pin = CONFIG_GPIO_DS18B20_ONEWIRE;
/** @brief Chamber temperature sensor address (from Kconfig). */
static const onewire_addr_t uChamperTempSensorAddr = CONFIG_ONEWIRE_ADDR_CHAMBER_TEMP;
/** @brief Outdoor temperature sensor address (from Kconfig). */
static const onewire_addr_t uOutdoorTempSensorAddr = CONFIG_ONEWIRE_ADDR_OUTDOOR_TEMP;
/** @brief Inlet flow temperature sensor address (from Kconfig). */
static const onewire_addr_t uInletFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_INLET_FLOW_TEMP;
/** @brief Return flow temperature sensor address (from Kconfig). */
static const onewire_addr_t uReturnFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_RETURN_FLOW_TEMP;
static onewire_addr_t uOneWireAddresses[MAX_DN18B20_SENSORS];
static float fDS18B20Temps[MAX_DN18B20_SENSORS];
static size_t sSensorCount = 0U;
static SemaphoreHandle_t xMutexAccessInputs = NULL;
static eBurnerErrorState sBurnerErrorState;
@ -32,29 +54,34 @@ static sMeasurement sOutdoorTemperature;
static sMeasurement sInletFlowTemperature;
static sMeasurement sReturnFlowTemperature;
void taskInput(void *pvParameters);
void initMeasurement(sMeasurement *pMeasurement);
void updateAverage(sMeasurement *pMeasurement);
void updatePrediction(sMeasurement *pMeasurement);
float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex);
/* Private function prototypes */
static void taskInput(void *pvParameters);
static void initMeasurement(sMeasurement *pMeasurement);
static void updateAverage(sMeasurement *pMeasurement);
static void updatePrediction(sMeasurement *pMeasurement);
static float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex);
void initInputs(void)
esp_err_t initInputs(void)
{
gpio_config_t ioConfBurnerFault = {
.pin_bit_mask = (1ULL << uBurnerFaultPin), // Pin mask
.mode = GPIO_MODE_INPUT, // Set as inout
.pull_up_en = GPIO_PULLUP_ENABLE, // Enable pull-up
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down
.intr_type = GPIO_INTR_DISABLE // Disable interrupts
};
.pin_bit_mask = (1ULL << uBurnerFaultPin),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
gpio_config(&ioConfBurnerFault);
esp_err_t ret = gpio_config(&ioConfBurnerFault);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret));
return ESP_FAIL;
}
xMutexAccessInputs = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessInputs == NULL)
{
ESP_LOGE(TAG, "Unable to create mutex");
ESP_LOGE(TAG, "Failed to create mutex");
return ESP_FAIL;
}
xSemaphoreGiveRecursive(xMutexAccessInputs);
@ -64,25 +91,28 @@ void initInputs(void)
initMeasurement(&sReturnFlowTemperature);
BaseType_t taskCreated = xTaskCreate(
taskInput, // Function to implement the task
"taskInput", // Task name
4096, // Stack size (in words, not bytes)
NULL, // Parameters to the task function (none in this case)
5, // Task priority (higher number = higher priority)
NULL // Task handle (optional)
);
taskInput,
"taskInput",
4096,
NULL,
5,
NULL);
if (taskCreated == pdPASS)
{
ESP_LOGI(TAG, "Task created successfully!");
}
else
if (taskCreated != pdPASS)
{
ESP_LOGE(TAG, "Failed to create task");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
}
void initMeasurement(sMeasurement *pMeasurement)
/**
* @brief Initialize a measurement structure to default values.
* @param pMeasurement Pointer to measurement structure.
*/
static void initMeasurement(sMeasurement *pMeasurement)
{
if (!pMeasurement)
return;
@ -94,25 +124,29 @@ void initMeasurement(sMeasurement *pMeasurement)
pMeasurement->average10s.fValue = INITIALISATION_VALUE;
pMeasurement->average10s.bufferCount = 0U;
pMeasurement->average10s.bufferIndex = 0U;
memset(pMeasurement->average10s.samples, 0U, AVG10S_SAMPLE_SIZE);
memset(pMeasurement->average10s.samples, 0U, sizeof(float) * AVG10S_SAMPLE_SIZE);
pMeasurement->average60s.fValue = INITIALISATION_VALUE;
pMeasurement->average60s.bufferCount = 0U;
pMeasurement->average60s.bufferIndex = 0U;
memset(pMeasurement->average60s.samples, 0U, AVG60S_SAMPLE_SIZE);
memset(pMeasurement->average60s.samples, 0U, sizeof(float) * AVG60S_SAMPLE_SIZE);
pMeasurement->predict60s.fValue = INITIALISATION_VALUE;
pMeasurement->predict60s.bufferCount = 0U;
pMeasurement->predict60s.bufferIndex = 0U;
memset(pMeasurement->predict60s.samples, 0U, PRED60S_SAMPLE_SIZE);
memset(pMeasurement->predict60s.samples, 0U, sizeof(float) * PRED60S_SAMPLE_SIZE);
}
void updateAverage(sMeasurement *pMeasurement)
/**
* @brief Update average values and damped value for a measurement.
* @param pMeasurement Pointer to measurement structure.
*/
static void updateAverage(sMeasurement *pMeasurement)
{
if (!pMeasurement)
return;
// Average form the last 10sec
/* 10-second average */
pMeasurement->average10s.samples[pMeasurement->average10s.bufferIndex] = pMeasurement->fCurrentValue;
pMeasurement->average10s.bufferIndex = (pMeasurement->average10s.bufferIndex + 1) % AVG10S_SAMPLE_SIZE;
@ -122,14 +156,21 @@ void updateAverage(sMeasurement *pMeasurement)
}
float sum = 0.0;
for (int i = 0; i <= pMeasurement->average10s.bufferCount; i++)
for (int i = 0; i < pMeasurement->average10s.bufferCount; i++)
{
sum += pMeasurement->average10s.samples[i];
}
pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount;
if (pMeasurement->average10s.bufferCount == 0U)
{
pMeasurement->average10s.fValue = 0.0f;
}
else
{
pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount;
}
// Average form the last 60sec
/* 60-second average */
pMeasurement->average60s.samples[pMeasurement->average60s.bufferIndex] = pMeasurement->fCurrentValue;
pMeasurement->average60s.bufferIndex = (pMeasurement->average60s.bufferIndex + 1) % AVG60S_SAMPLE_SIZE;
@ -144,9 +185,16 @@ void updateAverage(sMeasurement *pMeasurement)
sum += pMeasurement->average60s.samples[i];
}
pMeasurement->average60s.fValue = sum / pMeasurement->average60s.bufferCount;
if (pMeasurement->average60s.bufferCount == 0U)
{
pMeasurement->average60s.fValue = 0.0f;
}
else
{
pMeasurement->average60s.fValue = sum / pMeasurement->average60s.bufferCount;
}
// Damped current value
/* Damped current value */
if (pMeasurement->fDampedValue == INITIALISATION_VALUE)
{
pMeasurement->fDampedValue = pMeasurement->fCurrentValue;
@ -165,7 +213,11 @@ void updateAverage(sMeasurement *pMeasurement)
}
}
void updatePrediction(sMeasurement *pMeasurement)
/**
* @brief Update 60-second prediction using linear regression.
* @param pMeasurement Pointer to measurement structure.
*/
static void updatePrediction(sMeasurement *pMeasurement)
{
if (!pMeasurement)
return;
@ -185,7 +237,11 @@ void updatePrediction(sMeasurement *pMeasurement)
predict60s->bufferCount + 60.0f);
}
void taskInput(void *pvParameters)
/**
* @brief Input task - reads sensors periodically.
* @param pvParameters Task parameters (unused).
*/
static void taskInput(void *pvParameters)
{
while (1)
{
@ -287,20 +343,27 @@ void taskInput(void *pvParameters)
}
}
float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex)
/**
* @brief Predict future value using linear regression.
* @param samples Sample buffer.
* @param count Number of valid samples.
* @param bufferIndex Current buffer write index.
* @param futureIndex Future time index to predict.
* @return Predicted value.
*/
static float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex)
{
if (count == 0)
return INITIALISATION_VALUE; // No prediction possible with no data
return INITIALISATION_VALUE;
float sumX = INITIALISATION_VALUE, sumY = INITIALISATION_VALUE, sumXY = INITIALISATION_VALUE, sumX2 = INITIALISATION_VALUE;
for (size_t i = 0; i < count; i++)
{
// Calculate the circular buffer index for the current sample
size_t circularIndex = (bufferIndex + i + 1) % count;
float x = (float)i; // Time index
float y = samples[circularIndex]; // Sample value
float x = (float)i;
float y = samples[circularIndex];
sumX += x;
sumY += y;
@ -308,15 +371,13 @@ float linearRegressionPredict(const float *samples, size_t count, size_t bufferI
sumX2 += x * x;
}
// Calculate slope (m) and intercept (b) of the line: y = mx + b
float denominator = (count * sumX2 - sumX * sumX);
if (fabs(denominator) < 1e-6) // Avoid division by zero
return samples[bufferIndex]; // Return the latest value as prediction
if (fabs(denominator) < 1e-6)
return samples[bufferIndex];
float m = (count * sumXY - sumX * sumY) / denominator;
float b = (sumY - m * sumX) / count;
// Predict value at futureIndex
return m * futureIndex + b;
}
@ -397,4 +458,4 @@ eBurnerErrorState getBurnerError(void)
ESP_LOGE(TAG, "Unable to take mutex: getBurnerError()");
}
return ret;
}
}

View File

@ -1,55 +1,133 @@
/**
* @file inputs.h
* @brief Input handling for temperature sensors and burner fault detection.
*
* This module reads DS18B20 temperature sensors via 1-Wire and monitors
* the burner fault input. It provides averaged, damped, and predicted
* temperature values.
*/
#pragma once
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define INITIALISATION_VALUE 0.0f
#define AVG10S_SAMPLE_SIZE 10U
#define AVG60S_SAMPLE_SIZE 60U
#define AVG24H_SAMPLE_SIZE 24U
#define PRED60S_SAMPLE_SIZE 60U
#define DAMPING_FACTOR_WARMER 0.00001f // 0.001%
#define DAMPING_FACTOR_COLDER 0.00005f // 0.005%
#include "sdkconfig.h"
#include "esp_err.h"
#include <stddef.h>
/** @brief Returns the maximum of two values. */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
/** @brief Initial value for measurements before first reading. */
#define INITIALISATION_VALUE 0.0f
/** @brief Sample buffer size for 10-second average. */
#define AVG10S_SAMPLE_SIZE 10U
/** @brief Sample buffer size for 60-second average. */
#define AVG60S_SAMPLE_SIZE 60U
/** @brief Sample buffer size for 24-hour average. */
#define AVG24H_SAMPLE_SIZE 24U
/** @brief Sample buffer size for 60-second prediction. */
#define PRED60S_SAMPLE_SIZE 60U
/** @brief Damping factor for rising temperatures (from Kconfig). */
#define DAMPING_FACTOR_WARMER (CONFIG_DAMPING_FACTOR_WARMER * 0.00001f)
/** @brief Damping factor for falling temperatures (from Kconfig). */
#define DAMPING_FACTOR_COLDER (CONFIG_DAMPING_FACTOR_COLDER * 0.00001f)
/**
* @brief Burner error state enumeration.
*/
typedef enum _BurnerErrorState
{
NO_ERROR,
FAULT
NO_ERROR, /**< No burner fault detected. */
FAULT /**< Burner fault signal active. */
} eBurnerErrorState;
/**
* @brief Measurement error state enumeration.
*/
typedef enum _MeasurementErrorState
{
MEASUREMENT_NO_ERROR,
MEASUREMENT_FAULT
MEASUREMENT_NO_ERROR, /**< Measurement valid. */
MEASUREMENT_FAULT /**< Measurement failed or sensor not found. */
} eMeasurementErrorState;
/**
* @brief Circular buffer for averaging temperature values.
*/
typedef struct _Average
{
float fValue;
float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))];
size_t bufferIndex;
size_t bufferCount;
float fValue; /**< Current average value. */
float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))]; /**< Sample buffer. */
size_t bufferIndex; /**< Current write index. */
size_t bufferCount; /**< Number of valid samples. */
} sAverage;
/**
* @brief Circular buffer for temperature prediction.
*/
typedef struct _Predict
{
float fValue;
float samples[PRED60S_SAMPLE_SIZE];
size_t bufferIndex;
size_t bufferCount;
float fValue; /**< Predicted value. */
float samples[PRED60S_SAMPLE_SIZE]; /**< Sample buffer. */
size_t bufferIndex; /**< Current write index. */
size_t bufferCount; /**< Number of valid samples. */
} sPredict;
/**
* @brief Complete measurement data structure.
*/
typedef struct _Measurement
{
float fCurrentValue;
float fDampedValue;
sAverage average10s;
sAverage average60s;
sPredict predict60s;
eMeasurementErrorState state;
float fCurrentValue; /**< Current raw temperature value. */
float fDampedValue; /**< Damped temperature value. */
sAverage average10s; /**< 10-second rolling average. */
sAverage average60s; /**< 60-second rolling average. */
sPredict predict60s; /**< 60-second prediction. */
eMeasurementErrorState state; /**< Measurement state. */
} sMeasurement;
void initInputs(void);
/**
* @brief Initialize the inputs module.
*
* Configures GPIO for burner fault input and starts the input task
* for reading DS18B20 temperature sensors.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initInputs(void);
/**
* @brief Get the current chamber temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getChamberTemperature(void);
/**
* @brief Get the current outdoor temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getOutdoorTemperature(void);
/**
* @brief Get the current inlet flow temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getInletFlowTemperature(void);
/**
* @brief Get the current return flow temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getReturnFlowTemperature(void);
eBurnerErrorState getBurnerError(void);
/**
* @brief Get the current burner error state.
* @return eBurnerErrorState indicating fault status.
*/
eBurnerErrorState getBurnerError(void);

View File

@ -1,6 +1,10 @@
#include "esp_log.h"
#include <esp_system.h>
#include "nvs_flash.h"
/**
* @file main.c
* @brief Main entry point for Smart Oil Heating Control System.
*
* This file initializes all system modules and handles initialization
* errors by logging diagnostic information and triggering a reboot.
*/
#include "safety.h"
#include "metrics.h"
@ -10,33 +14,114 @@
#include "wifi.h"
#include "sntp.h"
static const char *TAG = "smart-oil-heater-control-system";
#include "esp_log.h"
#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)
{
ESP_LOGI(TAG, "starting ...");
ESP_LOGI(TAG, "========================================");
ESP_LOGI(TAG, "Smart Oil Heating Control System");
ESP_LOGI(TAG, "Starting initialization...");
ESP_LOGI(TAG, "========================================");
// Initialize NVS
/* Initialize NVS */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_LOGW(TAG, "NVS partition needs erase, erasing...");
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "NVS init failed: %s", esp_err_to_name(ret));
reboot_on_error("NVS");
}
ESP_LOGI(TAG, "[OK] NVS initialized");
// TODO: Error handling!
initOutputs();
initInputs();
initSafety();
initWifi();
initSntp();
initControl();
initMetrics();
/* Initialize Outputs */
if (initOutputs() != ESP_OK)
{
reboot_on_error("Outputs");
}
ESP_LOGI(TAG, "[OK] Outputs initialized");
/* Initialize Inputs */
if (initInputs() != ESP_OK)
{
reboot_on_error("Inputs");
}
ESP_LOGI(TAG, "[OK] Inputs initialized");
/* Initialize Safety */
if (initSafety() != ESP_OK)
{
reboot_on_error("Safety");
}
ESP_LOGI(TAG, "[OK] Safety initialized");
/* Initialize WiFi */
if (initWifi() != ESP_OK)
{
reboot_on_error("WiFi");
}
ESP_LOGI(TAG, "[OK] WiFi initialized");
/* Initialize SNTP */
if (initSntp() != ESP_OK)
{
reboot_on_error("SNTP");
}
ESP_LOGI(TAG, "[OK] SNTP initialized");
/* Initialize Control */
if (initControl() != ESP_OK)
{
reboot_on_error("Control");
}
ESP_LOGI(TAG, "[OK] Control initialized");
/* Initialize Metrics */
if (initMetrics() != ESP_OK)
{
reboot_on_error("Metrics");
}
ESP_LOGI(TAG, "[OK] Metrics initialized");
ESP_LOGI(TAG, "========================================");
ESP_LOGI(TAG, "All modules initialized successfully!");
ESP_LOGI(TAG, "System is now running.");
ESP_LOGI(TAG, "========================================");
while (1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
// Do nothing ;-)
}
}
}

View File

@ -1,11 +1,7 @@
#include <string.h>
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include <time.h>
#include <sys/time.h>
/**
* @file metrics.c
* @brief Implementation of Prometheus metrics endpoint.
*/
#include "metrics.h"
#include "outputs.h"
@ -14,41 +10,60 @@
#include "sntp.h"
#include "control.h"
static const char *TAG = "smart-oil-heater-control-system-metrics";
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
char caHtmlResponse[HTML_RESPONSE_SIZE];
SemaphoreHandle_t xMutexAccessMetricResponse = NULL;
#include <string.h>
#include <time.h>
#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 uint16_t u16MetricCounter = 0U;
void taskMetrics(void *pvParameters);
httpd_handle_t setup_server(void);
esp_err_t get_metrics_handler(httpd_req_t *req);
/* Private function prototypes */
static void taskMetrics(void *pvParameters);
static httpd_handle_t setup_server(void);
static esp_err_t get_metrics_handler(httpd_req_t *req);
void initMetrics(void)
esp_err_t initMetrics(void)
{
setup_server();
httpd_handle_t server = setup_server();
if (server == NULL)
{
ESP_LOGE(TAG, "Failed to start HTTP server");
return ESP_FAIL;
}
BaseType_t taskCreated = xTaskCreate(
taskMetrics, // Function to implement the task
"taskMetrics", // Task name
32768, // Stack size (in words, not bytes)
NULL, // Parameters to the task function (none in this case)
5, // Task priority (higher number = higher priority)
NULL // Task handle (optional)
);
taskMetrics,
"taskMetrics",
32768,
NULL,
5,
NULL);
if (taskCreated == pdPASS)
{
ESP_LOGI(TAG, "Task created successfully!");
}
else
if (taskCreated != pdPASS)
{
ESP_LOGE(TAG, "Failed to create task");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
}
void taskMetrics(void *pvParameters)
/**
* @brief Metrics collection task.
* @param pvParameters Task parameters (unused).
*/
static void taskMetrics(void *pvParameters)
{
while (1)
{
@ -301,23 +316,23 @@ void taskMetrics(void *pvParameters)
// Wifi RSSI
wifi_ap_record_t ap;
esp_wifi_sta_get_ap_info(&ap);
ap.rssi = 0U;
ESP_ERROR_CHECK(esp_wifi_sta_get_ap_info(&ap));
strcpy(aMetrics[u16MetricCounter].caMetricName, "wifi_rssi");
aMetrics[u16MetricCounter].type = INTEGER_64;
aMetrics[u16MetricCounter].i64MetricValue = ap.rssi;
u16MetricCounter++;
ESP_ERROR_CHECK(u16MetricCounter > METRIC_MAX_COUNT);
configASSERT(!(u16MetricCounter > METRIC_MAX_COUNT));
vSetMetrics(aMetrics, u16MetricCounter);
}
}
void vSetMetrics(sMetric *paMetrics, uint16_t u16Size)
{
if (xSemaphoreTakeRecursive(xMutexAccessMetricResponse, pdMS_TO_TICKS(5000)) == pdTRUE)
{
memset(caHtmlResponse, 0U, strlen(caHtmlResponse));
memset(caHtmlResponse, 0U, HTML_RESPONSE_SIZE);
for (uint16_t u16Index = 0U; u16Index < u16Size; u16Index++)
{
char caValueBuffer[64];
@ -337,8 +352,6 @@ void vSetMetrics(sMetric *paMetrics, uint16_t u16Size)
break;
}
// printf("%s\n", paMetrics[u16Index].caMetricName);
// printf("%s\n", caValueBuffer);
strcat(caHtmlResponse, paMetrics[u16Index].caMetricName);
strcat(caHtmlResponse, caValueBuffer);
strcat(caHtmlResponse, "\n");
@ -351,7 +364,12 @@ void vSetMetrics(sMetric *paMetrics, uint16_t u16Size)
}
}
esp_err_t get_metrics_handler(httpd_req_t *req)
/**
* @brief HTTP GET handler for /metrics endpoint.
* @param req HTTP request.
* @return ESP_OK on success.
*/
static esp_err_t get_metrics_handler(httpd_req_t *req)
{
if (xSemaphoreTakeRecursive(xMutexAccessMetricResponse, pdMS_TO_TICKS(5000)) == pdTRUE)
{
@ -366,7 +384,11 @@ esp_err_t get_metrics_handler(httpd_req_t *req)
}
}
httpd_handle_t setup_server(void)
/**
* @brief Setup HTTP server for metrics endpoint.
* @return HTTP server handle or NULL on failure.
*/
static httpd_handle_t setup_server(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 9100;
@ -381,14 +403,17 @@ httpd_handle_t setup_server(void)
xMutexAccessMetricResponse = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessMetricResponse == NULL)
{
ESP_LOGE(TAG, "Unable to create mutex for metric response");
ESP_LOGE(TAG, "Failed to create mutex");
return NULL;
}
xSemaphoreGiveRecursive(xMutexAccessMetricResponse);
if (httpd_start(&server, &config) == ESP_OK)
{
httpd_register_uri_handler(server, &uri_get);
return server;
}
return server;
ESP_LOGE(TAG, "Failed to start HTTP server");
return NULL;
}

View File

@ -1,26 +1,61 @@
/**
* @file metrics.h
* @brief Prometheus metrics HTTP endpoint.
*
* This module provides a HTTP server on port 9100 that exposes
* system metrics in Prometheus format at /metrics endpoint.
*/
#pragma once
#include <esp_http_server.h>
#include "esp_err.h"
#include "esp_http_server.h"
#include <stdint.h>
/** @brief Maximum size of HTTP response buffer. */
#define HTML_RESPONSE_SIZE 4096U
/** @brief Maximum length of metric name. */
#define METRIC_NAME_MAX_SIZE 64U
/** @brief Maximum number of metrics. */
#define METRIC_MAX_COUNT 38U
/**
* @brief Metric value type enumeration.
*/
typedef enum _MetricValueType
{
FLOAT,
INTEGER_U8,
INTEGER_64,
FLOAT, /**< Floating point value. */
INTEGER_U8, /**< 8-bit unsigned integer. */
INTEGER_64, /**< 64-bit signed integer. */
} eMetricValueType;
/**
* @brief Metric data structure.
*/
typedef struct _metric
{
char caMetricName[METRIC_NAME_MAX_SIZE];
eMetricValueType type;
float fMetricValue;
uint8_t u8MetricValue;
int64_t i64MetricValue;
char caMetricName[METRIC_NAME_MAX_SIZE]; /**< Metric name. */
eMetricValueType type; /**< Value type. */
float fMetricValue; /**< Float value (if type is FLOAT). */
uint8_t u8MetricValue; /**< U8 value (if type is INTEGER_U8). */
int64_t i64MetricValue; /**< I64 value (if type is INTEGER_64). */
} sMetric;
void initMetrics(void);
void vSetMetrics(sMetric *paMetrics, uint16_t u16Size);
/**
* @brief Initialize the metrics module.
*
* Starts the HTTP server and creates the metrics collection task.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initMetrics(void);
/**
* @brief Update the metrics buffer.
* @param paMetrics Array of metrics to publish.
* @param u16Size Number of metrics in array.
*/
void vSetMetrics(sMetric *paMetrics, uint16_t u16Size);

View File

@ -1,61 +1,101 @@
/**
* @file outputs.c
* @brief Implementation of output control module.
*/
#include "outputs.h"
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "outputs.h"
static const char *TAG = "outputs";
static const char *TAG = "smart-oil-heater-control-system-outputs";
const uint8_t uCirculationPumpGpioPin = 27U;
const uint8_t uBurnerGpioPin = 14U;
const uint8_t uSafetyContactGpioPin = 12U;
/** @brief Circulation pump GPIO pin (from Kconfig). */
static const uint8_t uCirculationPumpGpioPin = CONFIG_GPIO_CIRCULATION_PUMP;
/** @brief Burner control GPIO pin (from Kconfig). */
static const uint8_t uBurnerGpioPin = CONFIG_GPIO_BURNER;
/** @brief Safety contact GPIO pin (from Kconfig). */
static const uint8_t uSafetyContactGpioPin = CONFIG_GPIO_SAFETY_CONTACT;
static SemaphoreHandle_t xMutexAccessOutputs = NULL;
static eOutput sCirculationPumpState;
static eOutput sBurnerState;
static eOutput sSafetyContactState;
void initOutputs(void)
esp_err_t initOutputs(void)
{
gpio_config_t ioConfCirculationPump = {
.pin_bit_mask = (1ULL << uCirculationPumpGpioPin), // Pin mask
.mode = GPIO_MODE_OUTPUT, // Set as output
.pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down
.intr_type = GPIO_INTR_DISABLE // Disable interrupts
};
.pin_bit_mask = (1ULL << uCirculationPumpGpioPin),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
gpio_config_t ioConfBurner = {
.pin_bit_mask = (1ULL << uBurnerGpioPin), // Pin mask
.mode = GPIO_MODE_OUTPUT, // Set as output
.pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down
.intr_type = GPIO_INTR_DISABLE // Disable interrupts
};
.pin_bit_mask = (1ULL << uBurnerGpioPin),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
gpio_config_t ioConfSafetyContact = {
.pin_bit_mask = (1ULL << uSafetyContactGpioPin), // Pin mask
.mode = GPIO_MODE_OUTPUT, // Set as output
.pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down
.intr_type = GPIO_INTR_DISABLE // Disable interrupts
};
.pin_bit_mask = (1ULL << uSafetyContactGpioPin),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
gpio_config(&ioConfCirculationPump);
gpio_config(&ioConfBurner);
gpio_config(&ioConfSafetyContact);
esp_err_t ret = gpio_config(&ioConfCirculationPump);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "GPIO config failed 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();
if (xMutexAccessOutputs == NULL)
{
ESP_LOGE(TAG, "Unable to create mutex");
ESP_LOGE(TAG, "Failed to create mutex");
return ESP_FAIL;
}
xSemaphoreGiveRecursive(xMutexAccessOutputs);
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
}
eOutput getCirculationPumpState(void)
{
return sCirculationPumpState;
eOutput ret = ENABLED;
if (xSemaphoreTakeRecursive(xMutexAccessOutputs, pdMS_TO_TICKS(5000)) == pdTRUE)
{
ret = sCirculationPumpState;
xSemaphoreGiveRecursive(xMutexAccessOutputs);
}
else
{
ESP_LOGE(TAG, "Unable to take mutex: getCirculationPumpState()");
}
return ret;
}
void setCirculationPumpState(eOutput in)
@ -70,6 +110,7 @@ void setCirculationPumpState(eOutput in)
break;
case DISABLED:
gpio_set_level(uCirculationPumpGpioPin, 1U); // Switch off Circulation Pump
break;
default:
break;
}
@ -108,6 +149,7 @@ void setBurnerState(eOutput in)
break;
case DISABLED:
gpio_set_level(uBurnerGpioPin, 1U); // Switch off Burner
break;
default:
break;
}
@ -146,6 +188,7 @@ void setSafetyControlState(eOutput in)
break;
case DISABLED:
gpio_set_level(uSafetyContactGpioPin, 1U); // Switch off power for Burner
break;
default:
break;
}

View File

@ -1,15 +1,70 @@
/**
* @file outputs.h
* @brief Output control for circulation pump, burner, and safety contact.
*
* This module controls the relay outputs via GPIO pins. All outputs
* are active-low (relay energized when GPIO is low).
*/
#pragma once
#include "esp_err.h"
/**
* @brief Output state enumeration.
*/
typedef enum _Output
{
ENABLED,
DISABLED
ENABLED, /**< Output active (relay energized, GPIO low). */
DISABLED /**< Output inactive (relay de-energized, GPIO high). */
} eOutput;
void initOutputs(void);
/**
* @brief Initialize the outputs module.
*
* Configures GPIO pins for circulation pump, burner, and safety contact
* as outputs. All outputs are initialized to DISABLED (safe state).
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initOutputs(void);
/**
* @brief Get the current circulation pump state.
* @return eOutput state (ENABLED or DISABLED).
*/
eOutput getCirculationPumpState(void);
/**
* @brief Set the circulation pump state.
* @param in Desired state (ENABLED or DISABLED).
*/
void setCirculationPumpState(eOutput in);
/**
* @brief Get the current burner state.
* @return eOutput state (ENABLED or DISABLED).
*/
eOutput getBurnerState(void);
/**
* @brief Set the burner state.
* @param in Desired state (ENABLED or DISABLED).
*/
void setBurnerState(eOutput in);
/**
* @brief Get the current safety contact state.
* @return eOutput state (ENABLED or DISABLED).
*/
eOutput getSafetyControlState(void);
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,56 +1,79 @@
/**
* @file safety.c
* @brief Implementation of safety monitoring module.
*/
#include "safety.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <string.h>
#include "safety.h"
#include <math.h>
#define PERIODIC_INTERVAL 1U // run safety checks every 1sec
#define SENSOR_GRACE_PERIOD (60U * 30U) // period that a sensor can report the same reading in seconds
/** @brief Task interval in seconds. */
#define PERIODIC_INTERVAL 1U
/** @brief Grace period for unchanged sensor readings (seconds). */
#define SENSOR_GRACE_PERIOD (CONFIG_SENSOR_GRACE_PERIOD_MINUTES * 60U)
/** @brief Epsilon for float comparison. */
#define FLOAT_EPSILON 0.0001f
static const char *TAG = "safety";
static const char *TAG = "smart-oil-heater-control-system-safety";
static SemaphoreHandle_t xMutexAccessSafety = NULL;
/** @brief Sensor sanity check configurations. */
static sSensorSanityCheck sanityChecks[NUMBER_OF_SENSOR_SANITY_CHECKS] = {
{SENSOR_NO_ERROR, "chamber_temperature", {95.0f, -10.0f}, 0.0f, 0U, getChamberTemperature},
{SENSOR_NO_ERROR, "outdoor_temperature", {45.0f, -20.0f}, 0.0f, 0U, getOutdoorTemperature},
{SENSOR_NO_ERROR, "inlet_flow_temperature", {95.0f, -10.0f}, 0.0f, 0U, getInletFlowTemperature},
{SENSOR_NO_ERROR, "return_flow_temperature", {95.0f, -10.0f}, 0.0f, 0U, getReturnFlowTemperature}};
{SENSOR_NO_ERROR, "chamber_temperature", {SENSOR_LIMIT_CHAMBER_MAX, SENSOR_LIMIT_CHAMBER_MIN}, 0.0f, 0U, getChamberTemperature},
{SENSOR_NO_ERROR, "outdoor_temperature", {SENSOR_LIMIT_OUTDOOR_MAX, SENSOR_LIMIT_OUTDOOR_MIN}, 0.0f, 0U, getOutdoorTemperature},
{SENSOR_NO_ERROR, "inlet_flow_temperature", {SENSOR_LIMIT_INLET_MAX, SENSOR_LIMIT_INLET_MIN}, 0.0f, 0U, getInletFlowTemperature},
{SENSOR_NO_ERROR, "return_flow_temperature", {SENSOR_LIMIT_RETURN_MAX, SENSOR_LIMIT_RETURN_MIN}, 0.0f, 0U, getReturnFlowTemperature}};
static eSafetyState sSafetyState = SAFETY_NO_ERROR;
void taskSafety(void *pvParameters);
void checkSensorSanity(void);
void setSafeState(void);
/* Private function prototypes */
static void taskSafety(void *pvParameters);
static void checkSensorSanity(void);
static void setSafeState(void);
void initSafety(void)
esp_err_t initSafety(void)
{
xMutexAccessSafety = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessSafety == NULL)
{
ESP_LOGE(TAG, "Unable to create mutex");
ESP_LOGE(TAG, "Failed to create mutex");
return ESP_FAIL;
}
xSemaphoreGiveRecursive(xMutexAccessSafety);
BaseType_t taskCreated = xTaskCreate(
taskSafety, // Function to implement the task
"taskSafety", // Task name
4096, // Stack size (in words, not bytes)
NULL, // Parameters to the task function (none in this case)
5, // Task priority (higher number = higher priority)
NULL // Task handle (optional)
);
taskSafety,
"taskSafety",
4096,
NULL,
5,
NULL);
if (taskCreated == pdPASS)
{
ESP_LOGI(TAG, "Task created successfully!");
}
else
if (taskCreated != pdPASS)
{
ESP_LOGE(TAG, "Failed to create task");
return ESP_FAIL;
}
setSafeState(); // Set inital state
setSafeState();
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
}
void taskSafety(void *pvParameters)
/**
* @brief Safety monitoring task.
* @param pvParameters Task parameters (unused).
*/
static void taskSafety(void *pvParameters)
{
while (1)
{
@ -58,7 +81,6 @@ void taskSafety(void *pvParameters)
if (xSemaphoreTakeRecursive(xMutexAccessSafety, portMAX_DELAY) == pdTRUE)
{
checkSensorSanity();
if (sSafetyState != SAFETY_NO_ERROR)
@ -71,7 +93,10 @@ void taskSafety(void *pvParameters)
}
}
void checkSensorSanity(void)
/**
* @brief Check all sensor readings for sanity.
*/
static void checkSensorSanity(void)
{
sSafetyState = SAFETY_NO_ERROR;
for (int i = 0; i < NUMBER_OF_SENSOR_SANITY_CHECKS; i++)
@ -91,7 +116,7 @@ void checkSensorSanity(void)
}
else
{
if (sCurrentMeasurement.fCurrentValue == sanityChecks[i].fSensorTemperatureLast)
if (fabsf(sCurrentMeasurement.fCurrentValue - sanityChecks[i].fSensorTemperatureLast) < FLOAT_EPSILON)
{
sanityChecks[i].uUnchangedCounter++;
if (sanityChecks[i].uUnchangedCounter >= (SENSOR_GRACE_PERIOD / PERIODIC_INTERVAL))
@ -103,6 +128,7 @@ void checkSensorSanity(void)
}
else
{
sanityChecks[i].uUnchangedCounter = 0U;
sanityChecks[i].fSensorTemperatureLast = sCurrentMeasurement.fCurrentValue;
if (sCurrentMeasurement.fCurrentValue > sanityChecks[i].sSensorLimit.max)
@ -119,16 +145,17 @@ void checkSensorSanity(void)
}
else
{
sanityChecks[i].uUnchangedCounter = 0U;
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
setBurnerState(DISABLED); // Deactivate burner
@ -143,7 +170,7 @@ void getSensorSanityStates(sSensorSanityCheck *pSensorSanityChecks)
{
// Copy only the needed attributes
pSensorSanityChecks[i].state = sanityChecks[i].state;
strcpy(pSensorSanityChecks[i].name, sanityChecks[i].name);
strncpy(pSensorSanityChecks[i].name, sanityChecks[i].name, MAX_ERROR_STRING_SIZE);
}
xSemaphoreGiveRecursive(xMutexAccessSafety);
}

View File

@ -1,43 +1,117 @@
/**
* @file safety.h
* @brief Safety monitoring for temperature sensors.
*
* This module performs sanity checks on all temperature sensors and
* puts the system into a safe state if any sensor fails.
*/
#pragma once
#include "outputs.h"
#include "inputs.h"
#include "sdkconfig.h"
#include "esp_err.h"
#include <stdint.h>
/** @brief Maximum length of sensor name string. */
#define MAX_ERROR_STRING_SIZE 64U
/** @brief Number of sensors to monitor. */
#define NUMBER_OF_SENSOR_SANITY_CHECKS 4U
/** @brief Chamber sensor maximum temperature limit (°C). */
#define SENSOR_LIMIT_CHAMBER_MAX (CONFIG_SENSOR_LIMIT_CHAMBER_MAX / 10.0f)
/** @brief Chamber sensor minimum temperature limit (°C). */
#define SENSOR_LIMIT_CHAMBER_MIN (CONFIG_SENSOR_LIMIT_CHAMBER_MIN / 10.0f)
/** @brief Outdoor sensor maximum temperature limit (°C). */
#define SENSOR_LIMIT_OUTDOOR_MAX (CONFIG_SENSOR_LIMIT_OUTDOOR_MAX / 10.0f)
/** @brief Outdoor sensor minimum temperature limit (°C). */
#define SENSOR_LIMIT_OUTDOOR_MIN (CONFIG_SENSOR_LIMIT_OUTDOOR_MIN / 10.0f)
/** @brief Inlet flow sensor maximum temperature limit (°C). */
#define SENSOR_LIMIT_INLET_MAX (CONFIG_SENSOR_LIMIT_INLET_MAX / 10.0f)
/** @brief Inlet flow sensor minimum temperature limit (°C). */
#define SENSOR_LIMIT_INLET_MIN (CONFIG_SENSOR_LIMIT_INLET_MIN / 10.0f)
/** @brief Return flow sensor maximum temperature limit (°C). */
#define SENSOR_LIMIT_RETURN_MAX (CONFIG_SENSOR_LIMIT_RETURN_MAX / 10.0f)
/** @brief Return flow sensor minimum temperature limit (°C). */
#define SENSOR_LIMIT_RETURN_MIN (CONFIG_SENSOR_LIMIT_RETURN_MIN / 10.0f)
/**
* @brief Sensor error state enumeration.
*/
typedef enum _SensorErrorState
{
SENSOR_NO_ERROR,
SENSOR_TOO_HIGH,
SENSOR_TOO_LOW,
SENSOR_UNCHANGED,
SENSOR_NOT_FOUND
SENSOR_NO_ERROR, /**< Sensor operating normally. */
SENSOR_TOO_HIGH, /**< Temperature above maximum limit. */
SENSOR_TOO_LOW, /**< Temperature below minimum limit. */
SENSOR_UNCHANGED, /**< Temperature unchanged for too long. */
SENSOR_NOT_FOUND /**< Sensor not responding. */
} eSensorErrorState;
/**
* @brief Overall safety state enumeration.
*/
typedef enum _SafetyState
{
SAFETY_NO_ERROR,
SAFETY_SENSOR_ERROR,
SAFETY_INTERNAL_ERROR
SAFETY_NO_ERROR, /**< All sensors OK. */
SAFETY_SENSOR_ERROR, /**< At least one sensor failed. */
SAFETY_INTERNAL_ERROR /**< Internal module error. */
} eSafetyState;
/**
* @brief Function pointer type for sensor getter functions.
*/
typedef sMeasurement (*GetSensorValue)();
/**
* @brief Temperature sensor limits.
*/
typedef struct _TemperatureSensorLimit
{
float max; // Maximum temperature limit
float min; // Minimum temperature limit
float max; /**< Maximum temperature limit. */
float min; /**< Minimum temperature limit. */
} sTemperatureSensorLimit;
/**
* @brief Sensor sanity check state structure.
*/
typedef struct _SensorSanityCheck
{
eSensorErrorState state;
char name[MAX_ERROR_STRING_SIZE];
sTemperatureSensorLimit sSensorLimit;
float fSensorTemperatureLast;
uint32_t uUnchangedCounter;
GetSensorValue getSensor;
eSensorErrorState state; /**< Current error state. */
char name[MAX_ERROR_STRING_SIZE]; /**< Sensor name for logging. */
sTemperatureSensorLimit sSensorLimit; /**< Temperature limits. */
float fSensorTemperatureLast; /**< Last temperature reading. */
uint32_t uUnchangedCounter; /**< Counter for unchanged readings. */
GetSensorValue getSensor; /**< Function to get sensor value. */
} sSensorSanityCheck;
void initSafety(void);
/**
* @brief Initialize the safety module.
*
* Creates the safety monitoring task and sets initial safe state.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initSafety(void);
/**
* @brief Get the current sensor sanity states.
* @param[out] pSensorSanityChecks Array to receive sensor states.
*/
void getSensorSanityStates(sSensorSanityCheck *pSensorSanityChecks);
eSafetyState getSafetyState(void);
/**
* @brief Get the overall safety state.
* @return eSafetyState indicating current safety status.
*/
eSafetyState getSafetyState(void);

View File

@ -1,21 +1,32 @@
#include <time.h>
#include <sys/time.h>
#include <esp_sntp.h>
#include "esp_log.h"
/**
* @file sntp.c
* @brief Implementation of SNTP client module.
*/
#include "sntp.h"
static const char *TAG = "smart-oil-heater-control-system-sntp";
static eSntpState sntpState = SYNC_NOT_STARTED;
void time_sync_notification_cb(struct timeval *tv);
#include "esp_sntp.h"
#include "esp_log.h"
void initSntp(void)
#include <time.h>
#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_setservername(0, CONFIG_SNTP_SERVER_IP_ADDR);
sntp_set_time_sync_notification_cb(time_sync_notification_cb);
esp_sntp_init();
ESP_LOGI(TAG, "Initialized successfully, server: %s", CONFIG_SNTP_SERVER_IP_ADDR);
return ESP_OK;
}
eSntpState getSntpState(void)
@ -23,8 +34,12 @@ eSntpState getSntpState(void)
return sntpState;
}
void time_sync_notification_cb(struct timeval *tv)
/**
* @brief SNTP time sync callback.
* @param tv Synchronized time value.
*/
static void time_sync_notification_cb(struct timeval *tv)
{
ESP_LOGI(TAG, "SNTP synchronization! Unix Time: %lld", tv->tv_sec);
ESP_LOGI(TAG, "Time synchronized! Unix Time: %lld", tv->tv_sec);
sntpState = SYNC_SUCCESSFUL;
}

View File

@ -1,11 +1,37 @@
/**
* @file sntp.h
* @brief SNTP client for time synchronization.
*
* This module synchronizes system time with an NTP server.
* Time sync is required for schedule-based heating control.
*/
#pragma once
#include "esp_err.h"
/**
* @brief SNTP synchronization state enumeration.
*/
typedef enum _SntpState
{
SYNC_SUCCESSFUL,
SYNC_NOT_STARTED,
SYNC_FAILED,
SYNC_SUCCESSFUL, /**< Time synchronized successfully. */
SYNC_NOT_STARTED, /**< Synchronization not yet attempted. */
SYNC_FAILED, /**< Synchronization failed. */
} eSntpState;
void initSntp(void);
eSntpState getSntpState(void);
/**
* @brief Initialize the SNTP client.
*
* Configures SNTP with the server from Kconfig and starts
* periodic time synchronization.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initSntp(void);
/**
* @brief Get the current SNTP synchronization state.
* @return eSntpState indicating sync status.
*/
eSntpState getSntpState(void);

View File

@ -1,38 +1,62 @@
#include <string.h>
/**
* @file wifi.c
* @brief Implementation of WiFi station mode module.
*/
#include "wifi.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_netif.h"
#include <lwip/sockets.h>
#include "wifi.h"
#include <string.h>
/** @brief Event bit for successful connection. */
#define WIFI_CONNECTED_BIT BIT0
/** @brief Event bit for connection failure. */
#define WIFI_FAIL_BIT BIT1
static const char *TAG = "smart-oil-heater-control-system-wifi";
/** @brief Maximum connection retry attempts. */
#define MAX_RETRY_COUNT 10
/** @brief Delay between retries in milliseconds. */
#define RETRY_DELAY_MS 1000
static const char *TAG = "wifi";
static EventGroupHandle_t s_wifi_event_group;
static int s_retry_num = 0;
static bool s_initial_connect = true;
static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data);
void initWifi(void)
esp_err_t initWifi(void)
{
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_event_loop_create_default());
esp_netif_t *my_sta = esp_netif_create_default_wifi_sta();
esp_netif_dhcpc_stop(my_sta);
ESP_ERROR_CHECK(esp_netif_dhcpc_stop(my_sta));
esp_netif_ip_info_t ip_info;
ip_info.ip.addr = ipaddr_addr(CONFIG_STATIC_IP_ADDR);
ip_info.gw.addr = ipaddr_addr(CONFIG_STATIC_GATEWAY_IP_ADDR);
ip_info.netmask.addr = ipaddr_addr(CONFIG_STATIC_IP_NETMASK);
esp_netif_set_ip_info(my_sta, &ip_info);
ESP_ERROR_CHECK(esp_netif_set_ip_info(my_sta, &ip_info));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
@ -61,10 +85,10 @@ void initWifi(void)
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(78)); // Set max power to 19.5 dBm (78 in units of 0.25 dBm)
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); // Use power-saving mode
ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(78));
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM));
ESP_LOGI(TAG, "wifi_init_sta finished.");
ESP_LOGI(TAG, "WiFi init finished, waiting for connection...");
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
@ -74,19 +98,32 @@ void initWifi(void)
if (bits & WIFI_CONNECTED_BIT)
{
ESP_LOGI(TAG, "Connected to ap SSID:%s", CONFIG_SSID);
ESP_LOGI(TAG, "Connected to AP SSID:%s", CONFIG_SSID);
}
else if (bits & WIFI_FAIL_BIT)
{
ESP_LOGI(TAG, "Failed to connect to SSID:%s", CONFIG_SSID);
ESP_LOGE(TAG, "Failed to connect to SSID:%s", CONFIG_SSID);
return ESP_FAIL;
}
else
{
ESP_LOGE(TAG, "Unexpected event");
return ESP_FAIL;
}
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,
int32_t event_id, void *event_data)
{
@ -96,13 +133,46 @@ static void event_handler(void *arg, esp_event_base_t event_base,
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
esp_wifi_connect();
ESP_LOGI(TAG, "Retry to connect to the AP");
wifi_event_sta_disconnected_t *event = (wifi_event_sta_disconnected_t *)event_data;
ESP_LOGW(TAG, "Disconnected from AP (reason: %d)", event->reason);
if (s_initial_connect)
{
// During initial connection phase, use retry limit
if (s_retry_num < MAX_RETRY_COUNT)
{
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(TAG, "Retry to connect to the AP (%d/%d)", s_retry_num, MAX_RETRY_COUNT);
}
else
{
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
ESP_LOGE(TAG, "Failed to connect after %d attempts", MAX_RETRY_COUNT);
}
}
else
{
// After initial connection, always try to reconnect with delay
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
esp_wifi_connect();
ESP_LOGI(TAG, "Attempting to reconnect to the AP...");
}
}
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip));
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
s_retry_num = 0;
if (s_initial_connect)
{
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
else
{
ESP_LOGI(TAG, "Successfully reconnected to AP");
}
}
}
}

View File

@ -1,3 +1,22 @@
/**
* @file wifi.h
* @brief WiFi station mode initialization and management.
*
* This module initializes WiFi in station mode with static IP
* configuration. It handles connection and automatic reconnection.
*/
#pragma once
void initWifi(void);
#include "esp_err.h"
/**
* @brief Initialize WiFi in station mode.
*
* Configures WiFi with static IP address from Kconfig settings
* and connects to the configured access point. Blocks until
* connected or maximum retry count is reached.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initWifi(void);