Compare commits
21 Commits
main
...
feature/er
| Author | SHA256 | Date | |
|---|---|---|---|
| 4f355bdfdf | |||
| 430b4cb690 | |||
| 1d4e272d80 | |||
| f8f6af53bd | |||
| f3f6f1bc5f | |||
| b718073907 | |||
| d36b91a0fd | |||
| 40f757b7d1 | |||
| a9ec101bc6 | |||
| 0236ebcdd1 | |||
| 05757a5038 | |||
| 020eb63e05 | |||
| 67929580d5 | |||
| 10f9645580 | |||
| df3825df3a | |||
| 8c3dbc2886 | |||
| 267197ec20 | |||
| 781f9a1445 | |||
| 09a3c3a22d | |||
| 0775fda0ca | |||
| cd73985740 |
168
README.md
168
README.md
@ -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
|
||||||
|
```
|
||||||
@ -1,22 +1,391 @@
|
|||||||
menu "Smart Oil Heating Control System"
|
menu "Smart Oil Heating Control System"
|
||||||
|
|
||||||
|
menu "WiFi Configuration"
|
||||||
config SSID
|
config SSID
|
||||||
string "SSID"
|
string "WiFi SSID"
|
||||||
default "my WiFi SSID"
|
default "my WiFi SSID"
|
||||||
|
help
|
||||||
|
The SSID of the WiFi network to connect to.
|
||||||
|
|
||||||
config WIFI_PASSWORD
|
config WIFI_PASSWORD
|
||||||
string "WIFI_PASSWORD"
|
string "WiFi Password"
|
||||||
default "my WIFI Password"
|
default "my WIFI Password"
|
||||||
|
help
|
||||||
|
The password for the WiFi network.
|
||||||
|
|
||||||
config STATIC_IP_ADDR
|
config STATIC_IP_ADDR
|
||||||
string "Static IPv4 address"
|
string "Static IPv4 address"
|
||||||
default "192.168.0.42"
|
default "192.168.0.42"
|
||||||
|
help
|
||||||
|
Static IP address for the ESP32.
|
||||||
|
|
||||||
config STATIC_IP_NETMASK
|
config STATIC_IP_NETMASK
|
||||||
string "Static IPv4 netmask"
|
string "Static IPv4 netmask"
|
||||||
default "255.255.0.0"
|
default "255.255.0.0"
|
||||||
|
help
|
||||||
|
Network mask for the static IP configuration.
|
||||||
|
|
||||||
config STATIC_GATEWAY_IP_ADDR
|
config STATIC_GATEWAY_IP_ADDR
|
||||||
string "Static IPv4 gateway address"
|
string "Static IPv4 gateway address"
|
||||||
default "192.168.0.1"
|
default "192.168.0.1"
|
||||||
|
help
|
||||||
|
Gateway IP address for network routing.
|
||||||
|
|
||||||
config SNTP_SERVER_IP_ADDR
|
config SNTP_SERVER_IP_ADDR
|
||||||
string "SNTP IPv4 server address"
|
string "SNTP server address"
|
||||||
default "192.168.0.1"
|
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
|
||||||
|
|||||||
291
main/control.c
291
main/control.c
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void taskControl(void *pvParameters)
|
ESP_LOGI(TAG, "Initialized successfully");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Main control task.
|
||||||
|
* @param pvParameters Task parameters (unused).
|
||||||
|
*/
|
||||||
|
static void taskControl(void *pvParameters)
|
||||||
{
|
{
|
||||||
bool bHeatingInAction = false;
|
bool 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,12 +315,15 @@ void findControlCurrentTemperatureEntry(void)
|
|||||||
int currentHour = timeinfo.tm_hour;
|
int currentHour = timeinfo.tm_hour;
|
||||||
int currentMinute = timeinfo.tm_min;
|
int currentMinute = timeinfo.tm_min;
|
||||||
|
|
||||||
|
if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE)
|
||||||
|
{
|
||||||
|
|
||||||
// ESP_LOGI(TAG, "Searching for control entry - Day: %d, Time: %02d:%02d", currentDay, currentHour, currentMinute);
|
// ESP_LOGI(TAG, "Searching for control entry - Day: %d, Time: %02d:%02d", currentDay, currentHour, currentMinute);
|
||||||
|
|
||||||
// Search through all days and entries
|
// Search through all days and entries
|
||||||
for (int dayIndex = 0; dayIndex < 7; dayIndex++)
|
for (int dayIndex = 0; dayIndex < 7; dayIndex++)
|
||||||
{
|
{
|
||||||
const sControlDay *day = &aControlTable[dayIndex];
|
const sControlDay *day = &gControlTable[dayIndex];
|
||||||
|
|
||||||
for (int entryIndex = 0; entryIndex < day->entryCount; entryIndex++)
|
for (int entryIndex = 0; entryIndex < day->entryCount; entryIndex++)
|
||||||
{
|
{
|
||||||
@ -314,31 +338,33 @@ void findControlCurrentTemperatureEntry(void)
|
|||||||
|
|
||||||
if (isFutureDay || isTodayFutureTime)
|
if (isFutureDay || isTodayFutureTime)
|
||||||
{
|
{
|
||||||
|
|
||||||
// Found next scheduled entry, so determine the previous (active) one
|
// Found next scheduled entry, so determine the previous (active) one
|
||||||
if (entryIndex > 0)
|
if (entryIndex > 0)
|
||||||
{
|
{
|
||||||
// Use previous entry from same day
|
// Use previous entry from same day
|
||||||
currentControlEntry = day->aTemperatureEntries[entryIndex - 1];
|
gCurrentControlEntry = day->aTemperatureEntries[entryIndex - 1];
|
||||||
}
|
}
|
||||||
else if (dayIndex > 0)
|
else if (dayIndex > 0)
|
||||||
{
|
{
|
||||||
// Use last entry from previous day
|
// Use last entry from previous day
|
||||||
const sControlDay *previousDay = &aControlTable[dayIndex - 1];
|
const sControlDay *previousDay = &gControlTable[dayIndex - 1];
|
||||||
currentControlEntry = previousDay->aTemperatureEntries[previousDay->entryCount - 1];
|
gCurrentControlEntry = previousDay->aTemperatureEntries[previousDay->entryCount - 1];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// First entry of the week - wrap to last entry of Sunday
|
// First entry of the week - wrap to last entry of Sunday
|
||||||
const sControlDay *sunday = &aControlTable[6];
|
const sControlDay *sunday = &gControlTable[6];
|
||||||
currentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
|
gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
|
||||||
}
|
}
|
||||||
|
xSemaphoreGiveRecursive(xMutexAccessControl);
|
||||||
/*
|
/*
|
||||||
ESP_LOGI(TAG, "Active entry found - Time: %02d:%02d, "
|
ESP_LOGI(TAG, "Active entry found - Time: %02d:%02d, "
|
||||||
"Return Temp: %lf, Chamber Temp: %lf",
|
"Return Temp: %lf, Chamber Temp: %lf",
|
||||||
currentControlEntry.timestamp.hour,
|
gCurrentControlEntry.timestamp.hour,
|
||||||
currentControlEntry.timestamp.minute,
|
gCurrentControlEntry.timestamp.minute,
|
||||||
currentControlEntry.fReturnFlowTemperature,
|
gCurrentControlEntry.fReturnFlowTemperature,
|
||||||
currentControlEntry.fChamberTemperature);
|
gCurrentControlEntry.fChamberTemperature);
|
||||||
*/
|
*/
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -347,13 +373,30 @@ void findControlCurrentTemperatureEntry(void)
|
|||||||
|
|
||||||
// If we reached here, current time is after all entries this week
|
// If we reached here, current time is after all entries this week
|
||||||
// Use the last entry (Sunday evening)
|
// Use the last entry (Sunday evening)
|
||||||
const sControlDay *sunday = &aControlTable[6];
|
const sControlDay *sunday = &gControlTable[6];
|
||||||
currentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
|
gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
|
||||||
|
|
||||||
// ESP_LOGI(TAG, "Using last entry of week - Time: %02d:%02d", currentControlEntry.timestamp.hour, currentControlEntry.timestamp.minute);
|
// 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()");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
121
main/control.h
121
main/control.h
@ -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);
|
||||||
|
|||||||
195
main/inputs.c
195
main/inputs.c
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void initMeasurement(sMeasurement *pMeasurement)
|
ESP_LOGI(TAG, "Initialized successfully");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize a measurement structure to default values.
|
||||||
|
* @param pMeasurement Pointer to measurement structure.
|
||||||
|
*/
|
||||||
|
static void initMeasurement(sMeasurement *pMeasurement)
|
||||||
{
|
{
|
||||||
if (!pMeasurement)
|
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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pMeasurement->average10s.bufferCount == 0U)
|
||||||
|
{
|
||||||
|
pMeasurement->average10s.fValue = 0.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount;
|
pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount;
|
||||||
|
}
|
||||||
|
|
||||||
// Average form the last 60sec
|
/* 60-second average */
|
||||||
pMeasurement->average60s.samples[pMeasurement->average60s.bufferIndex] = pMeasurement->fCurrentValue;
|
pMeasurement->average60s.samples[pMeasurement->average60s.bufferIndex] = pMeasurement->fCurrentValue;
|
||||||
pMeasurement->average60s.bufferIndex = (pMeasurement->average60s.bufferIndex + 1) % AVG60S_SAMPLE_SIZE;
|
pMeasurement->average60s.bufferIndex = (pMeasurement->average60s.bufferIndex + 1) % AVG60S_SAMPLE_SIZE;
|
||||||
|
|
||||||
@ -144,9 +185,16 @@ void updateAverage(sMeasurement *pMeasurement)
|
|||||||
sum += pMeasurement->average60s.samples[i];
|
sum += pMeasurement->average60s.samples[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pMeasurement->average60s.bufferCount == 0U)
|
||||||
|
{
|
||||||
|
pMeasurement->average60s.fValue = 0.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
pMeasurement->average60s.fValue = sum / pMeasurement->average60s.bufferCount;
|
pMeasurement->average60s.fValue = sum / pMeasurement->average60s.bufferCount;
|
||||||
|
}
|
||||||
|
|
||||||
// Damped current value
|
/* Damped current value */
|
||||||
if (pMeasurement->fDampedValue == INITIALISATION_VALUE)
|
if (pMeasurement->fDampedValue == INITIALISATION_VALUE)
|
||||||
{
|
{
|
||||||
pMeasurement->fDampedValue = pMeasurement->fCurrentValue;
|
pMeasurement->fDampedValue = pMeasurement->fCurrentValue;
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
132
main/inputs.h
132
main/inputs.h
@ -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);
|
||||||
115
main/main.c
115
main/main.c
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
107
main/metrics.c
107
main/metrics.c
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void taskMetrics(void *pvParameters)
|
ESP_LOGI(TAG, "Initialized successfully");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Metrics collection task.
|
||||||
|
* @param pvParameters Task parameters (unused).
|
||||||
|
*/
|
||||||
|
static void taskMetrics(void *pvParameters)
|
||||||
{
|
{
|
||||||
while (1)
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
101
main/outputs.c
101
main/outputs.c
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
108
main/safety.h
108
main/safety.h
@ -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);
|
||||||
35
main/sntp.c
35
main/sntp.c
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
34
main/sntp.h
34
main/sntp.h
@ -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);
|
||||||
56
main/wifi.c
56
main/wifi.c
@ -1,22 +1,35 @@
|
|||||||
#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
|
||||||
|
|
||||||
|
/** @brief Maximum connection retry attempts. */
|
||||||
#define MAX_RETRY_COUNT 10
|
#define MAX_RETRY_COUNT 10
|
||||||
|
|
||||||
|
/** @brief Delay between retries in milliseconds. */
|
||||||
#define RETRY_DELAY_MS 1000
|
#define RETRY_DELAY_MS 1000
|
||||||
|
|
||||||
static const char *TAG = "smart-oil-heater-control-system-wifi";
|
static const char *TAG = "wifi";
|
||||||
|
|
||||||
static EventGroupHandle_t s_wifi_event_group;
|
static EventGroupHandle_t s_wifi_event_group;
|
||||||
static int s_retry_num = 0;
|
static int s_retry_num = 0;
|
||||||
@ -25,19 +38,25 @@ static bool s_initial_connect = true;
|
|||||||
static void event_handler(void *arg, esp_event_base_t event_base,
|
static void event_handler(void *arg, esp_event_base_t event_base,
|
||||||
int32_t event_id, void *event_data);
|
int32_t event_id, void *event_data);
|
||||||
|
|
||||||
void initWifi(void)
|
esp_err_t initWifi(void)
|
||||||
{
|
{
|
||||||
s_wifi_event_group = xEventGroupCreate();
|
s_wifi_event_group = xEventGroupCreate();
|
||||||
|
if (s_wifi_event_group == NULL)
|
||||||
|
{
|
||||||
|
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));
|
||||||
@ -66,10 +85,10 @@ void initWifi(void)
|
|||||||
|
|
||||||
ESP_ERROR_CHECK(esp_wifi_start());
|
ESP_ERROR_CHECK(esp_wifi_start());
|
||||||
|
|
||||||
ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(78)); // Set max power to 19.5 dBm (78 in units of 0.25 dBm)
|
ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(78));
|
||||||
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); // Use power-saving mode
|
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM));
|
||||||
|
|
||||||
ESP_LOGI(TAG, "wifi_init_sta finished.");
|
ESP_LOGI(TAG, "WiFi init finished, waiting for connection...");
|
||||||
|
|
||||||
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
|
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
|
||||||
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
|
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
|
||||||
@ -79,21 +98,32 @@ void initWifi(void)
|
|||||||
|
|
||||||
if (bits & WIFI_CONNECTED_BIT)
|
if (bits & WIFI_CONNECTED_BIT)
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "Connected to ap SSID:%s", CONFIG_SSID);
|
ESP_LOGI(TAG, "Connected to AP SSID:%s", CONFIG_SSID);
|
||||||
}
|
}
|
||||||
else if (bits & WIFI_FAIL_BIT)
|
else if (bits & WIFI_FAIL_BIT)
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "Failed to connect to SSID:%s", CONFIG_SSID);
|
ESP_LOGE(TAG, "Failed to connect to SSID:%s", CONFIG_SSID);
|
||||||
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ESP_LOGE(TAG, "Unexpected event");
|
ESP_LOGE(TAG, "Unexpected event");
|
||||||
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark initial connection phase complete - do NOT delete the event group
|
|
||||||
s_initial_connect = false;
|
s_initial_connect = false;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Initialized successfully");
|
||||||
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief WiFi event handler.
|
||||||
|
* @param arg User argument (unused).
|
||||||
|
* @param event_base Event base.
|
||||||
|
* @param event_id Event ID.
|
||||||
|
* @param event_data Event data.
|
||||||
|
*/
|
||||||
static void event_handler(void *arg, esp_event_base_t event_base,
|
static void event_handler(void *arg, esp_event_base_t event_base,
|
||||||
int32_t event_id, void *event_data)
|
int32_t event_id, void *event_data)
|
||||||
{
|
{
|
||||||
|
|||||||
21
main/wifi.h
21
main/wifi.h
@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user