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 1651 additions and 627 deletions

168
README.md
View File

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

View File

@ -1,36 +1,391 @@
menu "Smart Oil Heating Control System" menu "Smart Oil Heating Control System"
config SSID menu "WiFi Configuration"
string "SSID" config SSID
default "my WiFi SSID" string "WiFi SSID"
config WIFI_PASSWORD default "my WiFi SSID"
string "WIFI_PASSWORD" help
default "my WIFI Password" The SSID of the WiFi network to connect to.
config STATIC_IP_ADDR
string "Static IPv4 address" config WIFI_PASSWORD
default "192.168.0.42" string "WiFi Password"
config STATIC_IP_NETMASK default "my WIFI Password"
string "Static IPv4 netmask" help
default "255.255.0.0" The password for the WiFi network.
config STATIC_GATEWAY_IP_ADDR
string "Static IPv4 gateway address" config STATIC_IP_ADDR
default "192.168.0.1" string "Static IPv4 address"
config SNTP_SERVER_IP_ADDR default "192.168.0.42"
string "SNTP IPv4 server address" help
default "192.168.0.1" Static IP address for the ESP32.
config ENV_WIFI_BSSID_LOCK
bool "Lock to specific Access Point (BSSID)" config STATIC_IP_NETMASK
default n string "Static IPv4 netmask"
help default "255.255.0.0"
When enabled, the device will only connect to the access point help
with the specified MAC address (BSSID). Useful when multiple APs Network mask for the static IP configuration.
share the same SSID.
config ENV_WIFI_BSSID config STATIC_GATEWAY_IP_ADDR
string "Access Point MAC Address (BSSID)" string "Static IPv4 gateway address"
default "00:00:00:00:00:00" default "192.168.0.1"
depends on ENV_WIFI_BSSID_LOCK help
help Gateway IP address for network routing.
MAC address of the access point to connect to.
Format: XX:XX:XX:XX:XX:XX (uppercase or lowercase) 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 endmenu

View File

@ -1,134 +1,145 @@
/**
* @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"
#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 #include <time.h>
#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
static const char *TAG = "smart-oil-heater-control-system-control"; /** @brief Task interval in seconds. */
static eControlState sControlState = CONTROL_STARTING; #define PERIODIC_INTERVAL 1U
// Control table for daily schedules
static const sControlDay aControlTable[] = { static const char *TAG = "control";
static eControlState gControlState = CONTROL_STARTING;
/** @brief Weekly schedule table (from Kconfig). */
static const sControlDay gControlTable[] = {
{MONDAY, {MONDAY,
2U, 2U,
{{{4, 45}, {{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{22, 0}, {{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{TUESDAY, {TUESDAY,
2U, 2U,
{{{4, 45}, {{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{22, 0}, {{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{WEDNESDAY, {WEDNESDAY,
2U, 2U,
{{{4, 45}, {{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{22, 0}, {{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{THURSDAY, {THURSDAY,
2U, 2U,
{{{4, 45}, {{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{22, 0}, {{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{FRIDAY, {FRIDAY,
2U, 2U,
{{{4, 45}, {{{CONFIG_SCHEDULE_FRIDAY_DAY_START_HOUR, CONFIG_SCHEDULE_FRIDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{23, 0}, {{CONFIG_SCHEDULE_FRIDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_FRIDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{SATURDAY, {SATURDAY,
2U, 2U,
{{{6, 45}, {{{CONFIG_SCHEDULE_SATURDAY_DAY_START_HOUR, CONFIG_SCHEDULE_SATURDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{23, 30}, {{CONFIG_SCHEDULE_SATURDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_SATURDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET}}},
{SUNDAY, {SUNDAY,
2U, 2U,
{{{6, 45}, {{{CONFIG_SCHEDULE_SUNDAY_DAY_START_HOUR, CONFIG_SCHEDULE_SUNDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET}, CHAMBER_TEMPERATURE_TARGET},
{{22, 30}, {{CONFIG_SCHEDULE_SUNDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_SUNDAY_NIGHT_START_MINUTE},
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];
// Function prototypes static sControlTemperatureEntry gCurrentControlEntry =
void taskControl(void *pvParameters); gControlTable[0].aTemperatureEntries[0];
void findControlCurrentTemperatureEntry(void); 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 = xMutexAccessControl = xSemaphoreCreateRecursiveMutex();
xTaskCreate(taskControl, // Function to implement the task if (xMutexAccessControl == NULL)
"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_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"); 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 eBurnerState = BURNER_UNKNOWN; eBurnerState burnerState = 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!");
sControlState = CONTROL_FAULT_SAFETY; setControlState(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");
@ -139,11 +150,11 @@ 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!");
sControlState = CONTROL_FAULT_SNTP; setControlState(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");
@ -155,59 +166,51 @@ void taskControl(void *pvParameters)
} }
findControlCurrentTemperatureEntry(); findControlCurrentTemperatureEntry();
sControlTemperatureEntry currentControlEntry =
getControlCurrentTemperatureEntry();
if (getOutdoorTemperature().fDampedValue >= /* Summer mode hysteresis */
SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH) if (getOutdoorTemperature().fDampedValue >= SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH)
{ {
bSummerMode = true; bSummerMode = true;
} }
else if (getOutdoorTemperature().fDampedValue <= else if (getOutdoorTemperature().fDampedValue <= SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW)
SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW)
{ {
bSummerMode = false; bSummerMode = false;
} }
// Enable burner if outdoor temperature is low and return flow temperature /* Enable burner if needed */
// is cooled down if (!bHeatingInAction && (burnerState != BURNER_FAULT))
if (!bHeatingInAction && (eBurnerState != BURNER_FAULT))
{ {
if (bSummerMode) if (bSummerMode)
{ {
// ESP_LOGI(TAG, "Outdoor temperature too warm: Disabling heating");
setBurnerState(DISABLED); setBurnerState(DISABLED);
setSafetyControlState(DISABLED); setSafetyControlState(DISABLED);
sControlState = CONTROL_OUTDOOR_TOO_WARM; setControlState(CONTROL_OUTDOOR_TOO_WARM);
} }
else if ((getReturnFlowTemperature().average60s.fValue <= else if ((getReturnFlowTemperature().average60s.fValue <=
currentControlEntry.fReturnFlowTemperature) && getControlCurrentTemperatureEntry().fReturnFlowTemperature) &&
(getChamberTemperature().fCurrentValue <= (getChamberTemperature().fCurrentValue <= CHAMBER_TEMPERATURE_THRESHOLD))
CHAMBER_TEMPERATURE_THRESHOLD))
{ {
ESP_LOGI(TAG, ESP_LOGI(TAG, "Enabling burner: Return flow temperature target reached");
"Enabling burner: Return flow temperature target reached"); burnerState = BURNER_UNKNOWN;
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();
sControlState = CONTROL_HEATING; setControlState(CONTROL_HEATING);
} }
else else
{ {
// ESP_LOGI(TAG, "Return flow temperature too warm: Disabling heating"); setControlState(CONTROL_RETURN_FLOW_TOO_WARM);
sControlState = CONTROL_RETURN_FLOW_TOO_WARM;
} }
} }
// Disable burner if target temperature is reached or a fault occurred /* Disable burner if target reached or fault */
if (bHeatingInAction) if (bHeatingInAction)
{ {
if ((getChamberTemperature().fCurrentValue >= if ((getChamberTemperature().fCurrentValue >=
currentControlEntry.fChamberTemperature) || getControlCurrentTemperatureEntry().fChamberTemperature) ||
(getChamberTemperature().predict60s.fValue >= (getChamberTemperature().predict60s.fValue >=
currentControlEntry.fChamberTemperature)) getControlCurrentTemperatureEntry().fChamberTemperature))
{ {
ESP_LOGI(TAG, "Chamber target temperature reached: Disabling burner"); ESP_LOGI(TAG, "Chamber target temperature reached: Disabling burner");
bHeatingInAction = false; bHeatingInAction = false;
@ -217,14 +220,14 @@ 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 (eBurnerState == BURNER_UNKNOWN) if (burnerState == 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;
eBurnerState = BURNER_FAULT; burnerState = BURNER_FAULT;
sControlState = CONTROL_FAULT_BURNER; setControlState(CONTROL_FAULT_BURNER);
setBurnerState(DISABLED); setBurnerState(DISABLED);
setSafetyControlState(ENABLED); setSafetyControlState(ENABLED);
} }
@ -232,15 +235,14 @@ 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");
eBurnerState = BURNER_FIRED; burnerState = BURNER_FIRED;
} }
} }
} }
} }
// Manage circulation pump /* Manage circulation pump */
if (getChamberTemperature().fCurrentValue <= if (getChamberTemperature().fCurrentValue <= CIRCULATION_PUMP_TEMPERATURE_THRESHOLD)
CIRCULATION_PUMP_TEMPERATURE_THRESHOLD)
{ {
// ESP_LOGI(TAG, "Burner cooled down: Disabling circulation pump"); // ESP_LOGI(TAG, "Burner cooled down: Disabling circulation pump");
setCirculationPumpState(DISABLED); setCirculationPumpState(DISABLED);
@ -253,39 +255,58 @@ 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); 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); return (eControlWeekday)((day == 0) ? 6 : day - 1);
} }
/** /**
* @brief Finds the active temperature control entry for the current time. * @brief Find the currently active temperature entry based on time.
*
* Searches through the weekly schedule to find the most recent entry
* that should be active at the current date/time. Falls back to the
* last entry in the week if no suitable entry is found.
*/ */
/** static void findControlCurrentTemperatureEntry(void)
* @brief Finds the active temperature control entry for the current time.
*
* Searches through the weekly schedule to find the most recent entry
* that should be active at the current date/time. Falls back to the
* last entry in the week if no suitable entry is found.
*/
void findControlCurrentTemperatureEntry(void)
{ {
eControlWeekday currentDay = getControlCurrentWeekday(); eControlWeekday currentDay = getControlCurrentWeekday();
// Get current time
time_t now; time_t now;
struct tm timeinfo; struct tm timeinfo;
time(&now); time(&now);
@ -294,66 +315,88 @@ void findControlCurrentTemperatureEntry(void)
int currentHour = timeinfo.tm_hour; int currentHour = timeinfo.tm_hour;
int currentMinute = timeinfo.tm_min; int currentMinute = timeinfo.tm_min;
// ESP_LOGI(TAG, "Searching for control entry - Day: %d, Time: %02d:%02d", currentDay, currentHour, currentMinute); if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE)
// Search through all days and entries
for (int dayIndex = 0; dayIndex < 7; dayIndex++)
{ {
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) for (int entryIndex = 0; entryIndex < day->entryCount; entryIndex++)
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)
{ {
// Found next scheduled entry, so determine the previous (active) one const sControlTemperatureEntry *entry = &day->aTemperatureEntries[entryIndex];
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
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) 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 #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, CONTROL_STARTING, /**< System starting up. */
CONTROL_HEATING, CONTROL_HEATING, /**< Burner running. */
CONTROL_OUTDOOR_TOO_WARM, CONTROL_OUTDOOR_TOO_WARM, /**< Summer mode active. */
CONTROL_RETURN_FLOW_TOO_WARM, CONTROL_RETURN_FLOW_TOO_WARM, /**< Target temperature reached. */
CONTROL_FAULT_BURNER, CONTROL_FAULT_BURNER, /**< Burner fault detected. */
CONTROL_FAULT_SAFETY, CONTROL_FAULT_SAFETY, /**< Safety fault detected. */
CONTROL_FAULT_SNTP, CONTROL_FAULT_SNTP, /**< SNTP sync failed. */
} eControlState; } eControlState;
/**
* @brief Burner operational state enumeration.
*/
typedef enum _BurnerState typedef enum _BurnerState
{ {
BURNER_UNKNOWN, // Burner is disabled or state after enabling is still unkown BURNER_UNKNOWN, /**< Burner state unknown after enable. */
BURNER_FIRED, // Burner fired successfully BURNER_FIRED, /**< Burner fired successfully. */
BURNER_FAULT // Burner was unable to fire successfully BURNER_FAULT /**< Burner failed to fire. */
} eBurnerState; } eBurnerState;
/**
* @brief Weekday enumeration (Monday = 0).
*/
typedef enum _ControlWeekday typedef enum _ControlWeekday
{ {
MONDAY, MONDAY,
@ -32,27 +80,58 @@ typedef enum _ControlWeekday
SUNDAY, SUNDAY,
} eControlWeekday; } eControlWeekday;
/**
* @brief Time of day structure.
*/
typedef struct _ControlTimestamp typedef struct _ControlTimestamp
{ {
uint8_t hour; uint8_t hour; /**< Hour (0-23). */
uint8_t minute; uint8_t minute; /**< Minute (0-59). */
} sControlTimestamp; } sControlTimestamp;
/**
* @brief Temperature schedule entry.
*/
typedef struct _ControlTemperatureEntry typedef struct _ControlTemperatureEntry
{ {
sControlTimestamp timestamp; sControlTimestamp timestamp; /**< Time when entry becomes active. */
float fReturnFlowTemperature; float fReturnFlowTemperature; /**< Target return flow temperature. */
float fChamberTemperature; float fChamberTemperature; /**< Target chamber temperature. */
} sControlTemperatureEntry; } sControlTemperatureEntry;
/**
* @brief Daily schedule structure.
*/
typedef struct _ControlDay typedef struct _ControlDay
{ {
eControlWeekday day; eControlWeekday day; /**< Day of week. */
size_t entryCount; // number of entries for each day size_t entryCount; /**< Number of entries for this day. */
sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY]; sControlTemperatureEntry aTemperatureEntries[MAX_TEMPERATURE_ENTRIES_PER_DAY]; /**< Schedule entries. */
} sControlDay; } sControlDay;
void initControl(void); /**
* @brief Initialize the control module.
*
* Creates the control task that manages heating operation.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initControl(void);
/**
* @brief Get the current control state.
* @return eControlState indicating current operation mode.
*/
eControlState getControlState(void); eControlState getControlState(void);
/**
* @brief Get the current weekday.
* @return eControlWeekday (Monday = 0, Sunday = 6).
*/
eControlWeekday getControlCurrentWeekday(void); eControlWeekday getControlCurrentWeekday(void);
/**
* @brief Get the currently active temperature entry.
* @return sControlTemperatureEntry with current targets.
*/
sControlTemperatureEntry getControlCurrentTemperatureEntry(void); sControlTemperatureEntry getControlCurrentTemperatureEntry(void);

View File

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

View File

@ -1,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 #pragma once
#define MAX(a, b) ((a) > (b) ? (a) : (b)) #include "sdkconfig.h"
#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 "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 typedef enum _BurnerErrorState
{ {
NO_ERROR, NO_ERROR, /**< No burner fault detected. */
FAULT FAULT /**< Burner fault signal active. */
} eBurnerErrorState; } eBurnerErrorState;
/**
* @brief Measurement error state enumeration.
*/
typedef enum _MeasurementErrorState typedef enum _MeasurementErrorState
{ {
MEASUREMENT_NO_ERROR, MEASUREMENT_NO_ERROR, /**< Measurement valid. */
MEASUREMENT_FAULT MEASUREMENT_FAULT /**< Measurement failed or sensor not found. */
} eMeasurementErrorState; } eMeasurementErrorState;
/**
* @brief Circular buffer for averaging temperature values.
*/
typedef struct _Average typedef struct _Average
{ {
float fValue; float fValue; /**< Current average value. */
float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))]; float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))]; /**< Sample buffer. */
size_t bufferIndex; size_t bufferIndex; /**< Current write index. */
size_t bufferCount; size_t bufferCount; /**< Number of valid samples. */
} sAverage; } sAverage;
/**
* @brief Circular buffer for temperature prediction.
*/
typedef struct _Predict typedef struct _Predict
{ {
float fValue; float fValue; /**< Predicted value. */
float samples[PRED60S_SAMPLE_SIZE]; float samples[PRED60S_SAMPLE_SIZE]; /**< Sample buffer. */
size_t bufferIndex; size_t bufferIndex; /**< Current write index. */
size_t bufferCount; size_t bufferCount; /**< Number of valid samples. */
} sPredict; } sPredict;
/**
* @brief Complete measurement data structure.
*/
typedef struct _Measurement typedef struct _Measurement
{ {
float fCurrentValue; float fCurrentValue; /**< Current raw temperature value. */
float fDampedValue; float fDampedValue; /**< Damped temperature value. */
sAverage average10s; sAverage average10s; /**< 10-second rolling average. */
sAverage average60s; sAverage average60s; /**< 60-second rolling average. */
sPredict predict60s; sPredict predict60s; /**< 60-second prediction. */
eMeasurementErrorState state; eMeasurementErrorState state; /**< Measurement state. */
} sMeasurement; } sMeasurement;
void initInputs(void); /**
* @brief Initialize the inputs module.
*
* Configures GPIO for burner fault input and starts the input task
* for reading DS18B20 temperature sensors.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initInputs(void);
/**
* @brief Get the current chamber temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getChamberTemperature(void); sMeasurement getChamberTemperature(void);
/**
* @brief Get the current outdoor temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getOutdoorTemperature(void); sMeasurement getOutdoorTemperature(void);
/**
* @brief Get the current inlet flow temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getInletFlowTemperature(void); sMeasurement getInletFlowTemperature(void);
/**
* @brief Get the current return flow temperature measurement.
* @return sMeasurement structure with current values and state.
*/
sMeasurement getReturnFlowTemperature(void); sMeasurement getReturnFlowTemperature(void);
/**
* @brief Get the current burner error state.
* @return eBurnerErrorState indicating fault status.
*/
eBurnerErrorState getBurnerError(void); eBurnerErrorState getBurnerError(void);

View File

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

View File

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

View File

@ -1,26 +1,61 @@
/**
* @file metrics.h
* @brief Prometheus metrics HTTP endpoint.
*
* This module provides a HTTP server on port 9100 that exposes
* system metrics in Prometheus format at /metrics endpoint.
*/
#pragma once #pragma once
#include <esp_http_server.h> #include "esp_err.h"
#include "esp_http_server.h"
#include <stdint.h>
/** @brief Maximum size of HTTP response buffer. */
#define HTML_RESPONSE_SIZE 4096U #define HTML_RESPONSE_SIZE 4096U
/** @brief Maximum length of metric name. */
#define METRIC_NAME_MAX_SIZE 64U #define METRIC_NAME_MAX_SIZE 64U
/** @brief Maximum number of metrics. */
#define METRIC_MAX_COUNT 38U #define METRIC_MAX_COUNT 38U
/**
* @brief Metric value type enumeration.
*/
typedef enum _MetricValueType typedef enum _MetricValueType
{ {
FLOAT, FLOAT, /**< Floating point value. */
INTEGER_U8, INTEGER_U8, /**< 8-bit unsigned integer. */
INTEGER_64, INTEGER_64, /**< 64-bit signed integer. */
} eMetricValueType; } eMetricValueType;
/**
* @brief Metric data structure.
*/
typedef struct _metric typedef struct _metric
{ {
char caMetricName[METRIC_NAME_MAX_SIZE]; char caMetricName[METRIC_NAME_MAX_SIZE]; /**< Metric name. */
eMetricValueType type; eMetricValueType type; /**< Value type. */
float fMetricValue; float fMetricValue; /**< Float value (if type is FLOAT). */
uint8_t u8MetricValue; uint8_t u8MetricValue; /**< U8 value (if type is INTEGER_U8). */
int64_t i64MetricValue; int64_t i64MetricValue; /**< I64 value (if type is INTEGER_64). */
} sMetric; } sMetric;
void initMetrics(void); /**
* @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); 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/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"
#include "outputs.h" static const char *TAG = "outputs";
static const char *TAG = "smart-oil-heater-control-system-outputs"; /** @brief Circulation pump GPIO pin (from Kconfig). */
const uint8_t uCirculationPumpGpioPin = 27U; static const uint8_t uCirculationPumpGpioPin = CONFIG_GPIO_CIRCULATION_PUMP;
const uint8_t uBurnerGpioPin = 14U;
const uint8_t uSafetyContactGpioPin = 12U; /** @brief Burner control GPIO pin (from Kconfig). */
static const uint8_t uBurnerGpioPin = CONFIG_GPIO_BURNER;
/** @brief Safety contact GPIO pin (from Kconfig). */
static const uint8_t uSafetyContactGpioPin = CONFIG_GPIO_SAFETY_CONTACT;
static SemaphoreHandle_t xMutexAccessOutputs = NULL; static SemaphoreHandle_t xMutexAccessOutputs = NULL;
static eOutput sCirculationPumpState; static eOutput sCirculationPumpState;
static eOutput sBurnerState; static eOutput sBurnerState;
static eOutput sSafetyContactState; static eOutput sSafetyContactState;
void initOutputs(void) esp_err_t initOutputs(void)
{ {
gpio_config_t ioConfCirculationPump = { gpio_config_t ioConfCirculationPump = {
.pin_bit_mask = (1ULL << uCirculationPumpGpioPin), // Pin mask .pin_bit_mask = (1ULL << uCirculationPumpGpioPin),
.mode = GPIO_MODE_OUTPUT, // Set as output .mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up .pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down .pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE // Disable interrupts .intr_type = GPIO_INTR_DISABLE};
};
gpio_config_t ioConfBurner = { gpio_config_t ioConfBurner = {
.pin_bit_mask = (1ULL << uBurnerGpioPin), // Pin mask .pin_bit_mask = (1ULL << uBurnerGpioPin),
.mode = GPIO_MODE_OUTPUT, // Set as output .mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up .pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down .pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE // Disable interrupts .intr_type = GPIO_INTR_DISABLE};
};
gpio_config_t ioConfSafetyContact = { gpio_config_t ioConfSafetyContact = {
.pin_bit_mask = (1ULL << uSafetyContactGpioPin), // Pin mask .pin_bit_mask = (1ULL << uSafetyContactGpioPin),
.mode = GPIO_MODE_OUTPUT, // Set as output .mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE, // Disable pull-up .pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Disable pull-down .pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE // Disable interrupts .intr_type = GPIO_INTR_DISABLE};
};
gpio_config(&ioConfCirculationPump); esp_err_t ret = gpio_config(&ioConfCirculationPump);
gpio_config(&ioConfBurner); if (ret != ESP_OK)
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, "Unable to create mutex"); ESP_LOGE(TAG, "Failed to create mutex");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessOutputs); xSemaphoreGiveRecursive(xMutexAccessOutputs);
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
} }
eOutput getCirculationPumpState(void) eOutput getCirculationPumpState(void)
{ {
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) void setCirculationPumpState(eOutput in)
@ -70,6 +110,7 @@ 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;
} }
@ -108,6 +149,7 @@ 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;
} }
@ -146,6 +188,7 @@ 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,15 +1,70 @@
/**
* @file outputs.h
* @brief Output control for circulation pump, burner, and safety contact.
*
* This module controls the relay outputs via GPIO pins. All outputs
* are active-low (relay energized when GPIO is low).
*/
#pragma once #pragma once
#include "esp_err.h"
/**
* @brief Output state enumeration.
*/
typedef enum _Output typedef enum _Output
{ {
ENABLED, ENABLED, /**< Output active (relay energized, GPIO low). */
DISABLED DISABLED /**< Output inactive (relay de-energized, GPIO high). */
} eOutput; } eOutput;
void initOutputs(void); /**
* @brief Initialize the outputs module.
*
* Configures GPIO pins for circulation pump, burner, and safety contact
* as outputs. All outputs are initialized to DISABLED (safe state).
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initOutputs(void);
/**
* @brief Get the current circulation pump state.
* @return eOutput state (ENABLED or DISABLED).
*/
eOutput getCirculationPumpState(void); eOutput getCirculationPumpState(void);
/**
* @brief Set the circulation pump state.
* @param in Desired state (ENABLED or DISABLED).
*/
void setCirculationPumpState(eOutput in); void setCirculationPumpState(eOutput in);
/**
* @brief Get the current burner state.
* @return eOutput state (ENABLED or DISABLED).
*/
eOutput getBurnerState(void); eOutput getBurnerState(void);
/**
* @brief Set the burner state.
* @param in Desired state (ENABLED or DISABLED).
*/
void setBurnerState(eOutput in); void setBurnerState(eOutput in);
/**
* @brief Get the current safety contact state.
* @return eOutput state (ENABLED or DISABLED).
*/
eOutput getSafetyControlState(void); eOutput getSafetyControlState(void);
/**
* @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); 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/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 "safety.h" #include <math.h>
#define PERIODIC_INTERVAL 1U // run safety checks every 1sec /** @brief Task interval in seconds. */
#define SENSOR_GRACE_PERIOD (60U * 30U) // period that a sensor can report the same reading in seconds #define PERIODIC_INTERVAL 1U
/** @brief Grace period for unchanged sensor readings (seconds). */
#define SENSOR_GRACE_PERIOD (CONFIG_SENSOR_GRACE_PERIOD_MINUTES * 60U)
/** @brief Epsilon for float comparison. */
#define FLOAT_EPSILON 0.0001f
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", {95.0f, -10.0f}, 0.0f, 0U, getChamberTemperature}, {SENSOR_NO_ERROR, "chamber_temperature", {SENSOR_LIMIT_CHAMBER_MAX, SENSOR_LIMIT_CHAMBER_MIN}, 0.0f, 0U, getChamberTemperature},
{SENSOR_NO_ERROR, "outdoor_temperature", {45.0f, -20.0f}, 0.0f, 0U, getOutdoorTemperature}, {SENSOR_NO_ERROR, "outdoor_temperature", {SENSOR_LIMIT_OUTDOOR_MAX, SENSOR_LIMIT_OUTDOOR_MIN}, 0.0f, 0U, getOutdoorTemperature},
{SENSOR_NO_ERROR, "inlet_flow_temperature", {95.0f, -10.0f}, 0.0f, 0U, getInletFlowTemperature}, {SENSOR_NO_ERROR, "inlet_flow_temperature", {SENSOR_LIMIT_INLET_MAX, SENSOR_LIMIT_INLET_MIN}, 0.0f, 0U, getInletFlowTemperature},
{SENSOR_NO_ERROR, "return_flow_temperature", {95.0f, -10.0f}, 0.0f, 0U, getReturnFlowTemperature}}; {SENSOR_NO_ERROR, "return_flow_temperature", {SENSOR_LIMIT_RETURN_MAX, SENSOR_LIMIT_RETURN_MIN}, 0.0f, 0U, getReturnFlowTemperature}};
static eSafetyState sSafetyState = SAFETY_NO_ERROR; static eSafetyState sSafetyState = SAFETY_NO_ERROR;
void taskSafety(void *pvParameters); /* Private function prototypes */
void checkSensorSanity(void); static void taskSafety(void *pvParameters);
void setSafeState(void); static void checkSensorSanity(void);
static void setSafeState(void);
void initSafety(void) esp_err_t initSafety(void)
{ {
xMutexAccessSafety = xSemaphoreCreateRecursiveMutex(); xMutexAccessSafety = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessSafety == NULL) if (xMutexAccessSafety == NULL)
{ {
ESP_LOGE(TAG, "Unable to create mutex"); ESP_LOGE(TAG, "Failed to create mutex");
return ESP_FAIL;
} }
xSemaphoreGiveRecursive(xMutexAccessSafety); xSemaphoreGiveRecursive(xMutexAccessSafety);
BaseType_t taskCreated = xTaskCreate( BaseType_t taskCreated = xTaskCreate(
taskSafety, // Function to implement the task taskSafety,
"taskSafety", // Task name "taskSafety",
4096, // Stack size (in words, not bytes) 4096,
NULL, // Parameters to the task function (none in this case) NULL,
5, // Task priority (higher number = higher priority) 5,
NULL // Task handle (optional) NULL);
);
if (taskCreated == pdPASS) if (taskCreated != pdPASS)
{
ESP_LOGI(TAG, "Task created successfully!");
}
else
{ {
ESP_LOGE(TAG, "Failed to create task"); ESP_LOGE(TAG, "Failed to create task");
return ESP_FAIL;
} }
setSafeState(); // Set inital state setSafeState();
ESP_LOGI(TAG, "Initialized successfully");
return ESP_OK;
} }
void taskSafety(void *pvParameters) /**
* @brief Safety monitoring task.
* @param pvParameters Task parameters (unused).
*/
static void taskSafety(void *pvParameters)
{ {
while (1) while (1)
{ {
@ -58,7 +81,6 @@ void taskSafety(void *pvParameters)
if (xSemaphoreTakeRecursive(xMutexAccessSafety, portMAX_DELAY) == pdTRUE) if (xSemaphoreTakeRecursive(xMutexAccessSafety, portMAX_DELAY) == pdTRUE)
{ {
checkSensorSanity(); checkSensorSanity();
if (sSafetyState != SAFETY_NO_ERROR) if (sSafetyState != SAFETY_NO_ERROR)
@ -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; 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++)
@ -91,7 +116,7 @@ void checkSensorSanity(void)
} }
else else
{ {
if (sCurrentMeasurement.fCurrentValue == sanityChecks[i].fSensorTemperatureLast) if (fabsf(sCurrentMeasurement.fCurrentValue - sanityChecks[i].fSensorTemperatureLast) < FLOAT_EPSILON)
{ {
sanityChecks[i].uUnchangedCounter++; sanityChecks[i].uUnchangedCounter++;
if (sanityChecks[i].uUnchangedCounter >= (SENSOR_GRACE_PERIOD / PERIODIC_INTERVAL)) if (sanityChecks[i].uUnchangedCounter >= (SENSOR_GRACE_PERIOD / PERIODIC_INTERVAL))
@ -103,6 +128,7 @@ 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)
@ -119,16 +145,17 @@ 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
@ -143,7 +170,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;
strcpy(pSensorSanityChecks[i].name, sanityChecks[i].name); strncpy(pSensorSanityChecks[i].name, sanityChecks[i].name, MAX_ERROR_STRING_SIZE);
} }
xSemaphoreGiveRecursive(xMutexAccessSafety); 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 #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_NO_ERROR, /**< Sensor operating normally. */
SENSOR_TOO_HIGH, SENSOR_TOO_HIGH, /**< Temperature above maximum limit. */
SENSOR_TOO_LOW, SENSOR_TOO_LOW, /**< Temperature below minimum limit. */
SENSOR_UNCHANGED, SENSOR_UNCHANGED, /**< Temperature unchanged for too long. */
SENSOR_NOT_FOUND SENSOR_NOT_FOUND /**< Sensor not responding. */
} eSensorErrorState; } eSensorErrorState;
/**
* @brief Overall safety state enumeration.
*/
typedef enum _SafetyState typedef enum _SafetyState
{ {
SAFETY_NO_ERROR, SAFETY_NO_ERROR, /**< All sensors OK. */
SAFETY_SENSOR_ERROR, SAFETY_SENSOR_ERROR, /**< At least one sensor failed. */
SAFETY_INTERNAL_ERROR SAFETY_INTERNAL_ERROR /**< Internal module error. */
} eSafetyState; } eSafetyState;
/**
* @brief Function pointer type for sensor getter functions.
*/
typedef sMeasurement (*GetSensorValue)(); typedef sMeasurement (*GetSensorValue)();
/**
* @brief Temperature sensor limits.
*/
typedef struct _TemperatureSensorLimit typedef struct _TemperatureSensorLimit
{ {
float max; // Maximum temperature limit float max; /**< Maximum temperature limit. */
float min; // Minimum temperature limit float min; /**< Minimum temperature limit. */
} sTemperatureSensorLimit; } sTemperatureSensorLimit;
/**
* @brief Sensor sanity check state structure.
*/
typedef struct _SensorSanityCheck typedef struct _SensorSanityCheck
{ {
eSensorErrorState state; eSensorErrorState state; /**< Current error state. */
char name[MAX_ERROR_STRING_SIZE]; char name[MAX_ERROR_STRING_SIZE]; /**< Sensor name for logging. */
sTemperatureSensorLimit sSensorLimit; sTemperatureSensorLimit sSensorLimit; /**< Temperature limits. */
float fSensorTemperatureLast; float fSensorTemperatureLast; /**< Last temperature reading. */
uint32_t uUnchangedCounter; uint32_t uUnchangedCounter; /**< Counter for unchanged readings. */
GetSensorValue getSensor; GetSensorValue getSensor; /**< Function to get sensor value. */
} sSensorSanityCheck; } sSensorSanityCheck;
void initSafety(void); /**
* @brief Initialize the safety module.
*
* Creates the safety monitoring task and sets initial safe state.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initSafety(void);
/**
* @brief Get the current sensor sanity states.
* @param[out] pSensorSanityChecks Array to receive sensor states.
*/
void getSensorSanityStates(sSensorSanityCheck *pSensorSanityChecks); void getSensorSanityStates(sSensorSanityCheck *pSensorSanityChecks);
/**
* @brief Get the overall safety state.
* @return eSafetyState indicating current safety status.
*/
eSafetyState getSafetyState(void); eSafetyState getSafetyState(void);

View File

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

View File

@ -1,11 +1,37 @@
/**
* @file sntp.h
* @brief SNTP client for time synchronization.
*
* This module synchronizes system time with an NTP server.
* Time sync is required for schedule-based heating control.
*/
#pragma once #pragma once
#include "esp_err.h"
/**
* @brief SNTP synchronization state enumeration.
*/
typedef enum _SntpState typedef enum _SntpState
{ {
SYNC_SUCCESSFUL, SYNC_SUCCESSFUL, /**< Time synchronized successfully. */
SYNC_NOT_STARTED, SYNC_NOT_STARTED, /**< Synchronization not yet attempted. */
SYNC_FAILED, SYNC_FAILED, /**< Synchronization failed. */
} eSntpState; } eSntpState;
void initSntp(void); /**
* @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); eSntpState getSntpState(void);

View File

@ -1,39 +1,62 @@
#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 "wifi.h" #include <string.h>
/** @brief Event bit for successful connection. */
#define WIFI_CONNECTED_BIT BIT0 #define WIFI_CONNECTED_BIT BIT0
/** @brief Event bit for connection failure. */
#define WIFI_FAIL_BIT BIT1 #define WIFI_FAIL_BIT BIT1
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 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);
void initWifi(void) esp_err_t 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_netif_dhcpc_stop(my_sta); ESP_ERROR_CHECK(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_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(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_init(&cfg));
@ -57,30 +80,15 @@ void 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)); // Set max power to 19.5 dBm (78 in units of 0.25 dBm) ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(78));
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); // Use power-saving mode ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM));
ESP_LOGI(TAG, "wifi_init_sta finished."); ESP_LOGI(TAG, "WiFi init finished, waiting for connection...");
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
@ -90,19 +98,32 @@ void initWifi(void)
if (bits & WIFI_CONNECTED_BIT) if (bits & WIFI_CONNECTED_BIT)
{ {
ESP_LOGI(TAG, "Connected to ap SSID:%s", CONFIG_SSID); ESP_LOGI(TAG, "Connected to AP SSID:%s", CONFIG_SSID);
} }
else if (bits & WIFI_FAIL_BIT) else if (bits & WIFI_FAIL_BIT)
{ {
ESP_LOGI(TAG, "Failed to connect to SSID:%s", CONFIG_SSID); ESP_LOGE(TAG, "Failed to connect to SSID:%s", CONFIG_SSID);
return ESP_FAIL;
} }
else else
{ {
ESP_LOGE(TAG, "Unexpected event"); ESP_LOGE(TAG, "Unexpected event");
return ESP_FAIL;
} }
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)
{ {
@ -112,37 +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) else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{ {
esp_wifi_connect(); wifi_event_sta_disconnected_t *event = (wifi_event_sta_disconnected_t *)event_data;
ESP_LOGI(TAG, "Retry to connect to the AP"); 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) 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));
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); s_retry_num = 0;
}
}
/** if (s_initial_connect)
* @brief Parse BSSID string to byte array {
* xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
* @param bssid_str BSSID string in format "XX:XX:XX:XX:XX:XX" }
* @param bssid Output byte array (6 bytes) else
* @return true on success, false on parse error {
*/ ESP_LOGI(TAG, "Successfully reconnected to AP");
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,3 +1,22 @@
/**
* @file wifi.h
* @brief WiFi station mode initialization and management.
*
* This module initializes WiFi in station mode with static IP
* configuration. It handles connection and automatic reconnection.
*/
#pragma once #pragma once
void initWifi(void); #include "esp_err.h"
/**
* @brief Initialize WiFi in station mode.
*
* Configures WiFi with static IP address from Kconfig settings
* and connects to the configured access point. Blocks until
* connected or maximum retry count is reached.
*
* @return ESP_OK on success, ESP_FAIL on error.
*/
esp_err_t initWifi(void);