41 Commits

Author SHA256 Message Date
f8f6af53bd implement config 2026-01-10 12:50:06 +01:00
f3f6f1bc5f Potential division by zero 2026-01-10 12:01:22 +01:00
b718073907 Missing break before default 2026-01-10 11:58:46 +01:00
d36b91a0fd Variable name shadows type name 2026-01-10 11:57:15 +01:00
40f757b7d1 uUnchangedCounter reset logic flaw 2026-01-10 11:54:18 +01:00
a9ec101bc6 Floating-point equality comparison 2026-01-10 11:52:08 +01:00
0236ebcdd1 Unsafe strcpy 2026-01-10 11:47:04 +01:00
05757a5038 Unchecked WiFi API call 2026-01-10 11:45:49 +01:00
020eb63e05 Unchecked network configuration 2026-01-10 11:43:26 +01:00
67929580d5 Unchecked xEventGroupCreate 2026-01-10 11:42:27 +01:00
10f9645580 Unchecked gpio_config returns 2026-01-10 11:39:37 +01:00
df3825df3a Non-thread-safe function 2026-01-10 11:33:37 +01:00
8c3dbc2886 Unprotected shared state access 2026-01-10 11:31:34 +01:00
267197ec20 Missing mutex protection 2026-01-10 11:06:10 +01:00
781f9a1445 ncorrect memset with strlen 2026-01-10 11:02:31 +01:00
09a3c3a22d Misuse of ESP_ERROR_CHECK 2026-01-10 10:58:12 +01:00
0775fda0ca Off-by-one error (buffer overread) 2026-01-10 10:55:15 +01:00
cd73985740 Wrong memset size 2026-01-10 10:54:32 +01:00
af307fd403 handle reconnect 2026-01-09 23:35:44 +01:00
cb69bea618 Update control entry (#25)
Reviewed-on: #25
Co-authored-by: localhorst <localhorst@mosad.xyz>
Co-committed-by: localhorst <localhorst@mosad.xyz>
2026-01-09 23:17:26 +01:00
dddf2c9bf0 Merge pull request 'Disable heating in summer' (#22) from feature/summer-mode into main
Reviewed-on: #22
2025-10-24 17:38:20 +02:00
33c7bc4007 use damped value as event source 2025-10-24 17:23:47 +02:00
524d94c515 Export current entry temperatures as metrics 2025-10-24 16:19:49 +02:00
e8c62a1bd7 slow down damping 2025-10-24 15:35:23 +02:00
5b987bfd5b improve damping 2025-10-24 11:14:10 +02:00
b3a571da3f damping instead of 24h average 2025-10-18 20:39:36 +02:00
9ff3b38f70 disable avg24h 2025-10-18 19:26:14 +02:00
067dc84afa average value for last 24h 2025-10-12 13:10:26 +02:00
9974e2d738 detect summer mode based on two thresholds 2025-10-12 12:59:33 +02:00
6eca00200e suppress heating in summer 2025-07-12 12:04:08 +02:00
ac15376f6b spelling fixes 2025-04-25 21:52:31 +02:00
dcace073d9 rework circulation pump 2025-04-25 21:45:59 +02:00
da7a1be183 Merge pull request 'fix/burner-fault-detection' (#23) from fix/burner-fault-detection into feature/summer-mode
Reviewed-on: #23
2025-04-25 20:45:51 +02:00
2477ccb42a increase threshold 2025-04-19 08:48:57 +02:00
f66b831666 rework burner fault detection 2025-04-19 08:36:19 +02:00
66b7f8320e increase burner fault threshold 2025-04-18 17:50:20 +02:00
416cda0f50 disable log of event 2025-03-01 15:43:29 +01:00
8ca3d97165 refactoring 2025-03-01 15:36:05 +01:00
c9b7313608 refactor 2025-03-01 15:24:48 +01:00
fa958dd53b add outdoor threshold 2025-03-01 15:17:22 +01:00
3771a83fcc change onewire addr to new outdoor sensor 2025-03-01 14:27:12 +01:00
14 changed files with 1029 additions and 257 deletions

View File

@ -19,7 +19,11 @@ Sntp <|-- Metrics
class Inputs{ class Inputs{
+initInputs() +initInputs()
-initMeasurement()
-updateAverage()
-updatePrediction()
-taskInput() -taskInput()
-linearRegressionPredict()
+getChamberTemperature() +getChamberTemperature()
+getOutdoorTemperature() +getOutdoorTemperature()
+getInletFlowTemperature() +getInletFlowTemperature()
@ -38,7 +42,11 @@ Sntp <|-- Metrics
} }
class Control{ class Control{
initControl()
+taskControl() +taskControl()
+getControlCurrentWeekday()
-findControlCurrentTemperatureEntry()
+getControlCurrentTemperatureEntry()
-controlTable -controlTable
+getControlState() +getControlState()
} }
@ -79,33 +87,41 @@ Sntp <|-- Metrics
burner_fault_pending 1 burner_fault_pending 1
circulation_pump_enabled 1 circulation_pump_enabled 1
burner_enabled 0 burner_enabled 0
safety_contact_enabled 0 safety_contact_enabled 1
chamber_temperature 58.750000 chamber_temperature 37.250000
chamber_temperature_avg10 58.931252 chamber_temperature_avg10 37.237499
chamber_temperature_avg60 59.190475 chamber_temperature_avg60 37.438541
chamber_temperature_pred60 55.870998 chamber_temperature_damped 42.185040
inlet_flow_temperature 53.875000 chamber_temperature_pred60 36.638443
inlet_flow_temperature_avg10 53.900002 inlet_flow_temperature 35.625000
inlet_flow_temperature_avg60 53.994320 inlet_flow_temperature_avg10 35.618752
inlet_flow_temperature_pred60 52.848743 inlet_flow_temperature_avg60 35.415627
outdoor_temperature 18.000000 inlet_flow_temperature_damped 39.431259
outdoor_temperature_avg10 18.006250 inlet_flow_temperature_pred60 36.078678
outdoor_temperature_avg60 18.002840 outdoor_temperature 14.687500
outdoor_temperature_pred60 18.050785 outdoor_temperature_avg10 14.662500
return_flow_temperature 48.625000 outdoor_temperature_avg60 14.646875
return_flow_temperature_avg10 48.718750 outdoor_temperature_damped 9.169084
return_flow_temperature_avg60 48.846592 outdoor_temperature_pred60 14.660233
return_flow_temperature_pred60 47.383083 return_flow_temperature 39.937500
return_flow_temperature_avg10 40.087502
return_flow_temperature_avg60 41.146873
return_flow_temperature_damped 32.385151
return_flow_temperature_pred60 37.311958
chamber_temperature_state 0 chamber_temperature_state 0
outdoor_temperature_state 0 outdoor_temperature_state 0
inlet_flow_temperature_state 0 inlet_flow_temperature_state 0
return_flow_temperature_state 0 return_flow_temperature_state 0
safety_state 0 safety_state 0
control_state 3 control_state 3
control_current_weekday 5
control_current_entry_time 17100
control_current_entry_chamber_temperature 80.000000
control_current_entry_return_flow_temperature 30.000000
sntp_state 0 sntp_state 0
system_unixtime 1735242392 system_unixtime 1762012743
uptime_seconds 40 uptime_seconds 465229
wifi_rssi -74 wifi_rssi -72
``` ```
#### Status Encoding #### Status Encoding

View File

@ -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

View File

@ -1,41 +1,98 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "control.h" #include "control.h"
#include "outputs.h"
#include "inputs.h" #include "inputs.h"
#include "outputs.h"
#include "safety.h" #include "safety.h"
#include "sntp.h" #include "sntp.h"
#define PERIODIC_INTERVAL 1U // run control loop every 1sec #include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY 30.0 #define PERIODIC_INTERVAL 1U // Run control loop every 1 second
#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT 25.0
#define CHAMPER_TEMPERATURE_TARGET 80.0
#define BURNER_FAULT_DETECTION_THRESHOLD (60U * 3U) // Detect burner fault if after 3 minutes no burner start detected
static const char *TAG = "smart-oil-heater-control-system-control"; static const char *TAG = "smart-oil-heater-control-system-control";
static eControlState sControlState = CONTROL_STARTING; static eControlState gControlState = CONTROL_STARTING;
// Control table for daily schedules
static sControlDay aControlTable[] = { static const sControlDay gControlTable[] = {
{MONDAY, 2U, {{{4, 45}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMPER_TEMPERATURE_TARGET}, {{22, 0}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMPER_TEMPERATURE_TARGET}}}, {MONDAY,
{TUESDAY, 2U, {{{4, 45}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMPER_TEMPERATURE_TARGET}, {{22, 0}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMPER_TEMPERATURE_TARGET}}}, 2U,
{WEDNESDAY, 2U, {{{4, 45}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMPER_TEMPERATURE_TARGET}, {{22, 0}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMPER_TEMPERATURE_TARGET}}}, {{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE},
{THURSDAY, 2U, {{{4, 45}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMPER_TEMPERATURE_TARGET}, {{22, 0}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMPER_TEMPERATURE_TARGET}}}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
{FRIDAY, 2U, {{{4, 45}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMPER_TEMPERATURE_TARGET}, {{23, 0}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMPER_TEMPERATURE_TARGET}}}, CHAMBER_TEMPERATURE_TARGET},
{SATURDAY, 2U, {{{6, 45}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMPER_TEMPERATURE_TARGET}, {{23, 30}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMPER_TEMPERATURE_TARGET}}}, {{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE},
{SUNDAY, 2U, {{{6, 45}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY, CHAMPER_TEMPERATURE_TARGET}, {{22, 30}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT, CHAMPER_TEMPERATURE_TARGET}}}, RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}},
{TUESDAY,
2U,
{{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}},
{WEDNESDAY,
2U,
{{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}},
{THURSDAY,
2U,
{{{CONFIG_SCHEDULE_WEEKDAY_DAY_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_WEEKDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}},
{FRIDAY,
2U,
{{{CONFIG_SCHEDULE_FRIDAY_DAY_START_HOUR, CONFIG_SCHEDULE_FRIDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_FRIDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_FRIDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}},
{SATURDAY,
2U,
{{{CONFIG_SCHEDULE_SATURDAY_DAY_START_HOUR, CONFIG_SCHEDULE_SATURDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_SATURDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_SATURDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}},
{SUNDAY,
2U,
{{{CONFIG_SCHEDULE_SUNDAY_DAY_START_HOUR, CONFIG_SCHEDULE_SUNDAY_DAY_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY,
CHAMBER_TEMPERATURE_TARGET},
{{CONFIG_SCHEDULE_SUNDAY_NIGHT_START_HOUR, CONFIG_SCHEDULE_SUNDAY_NIGHT_START_MINUTE},
RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT,
CHAMBER_TEMPERATURE_TARGET}}},
}; };
static sControlTemperatureEntry gCurrentControlEntry =
gControlTable[0].aTemperatureEntries[0];
static SemaphoreHandle_t xMutexAccessControl = NULL;
// Function prototypes
void taskControl(void *pvParameters); void taskControl(void *pvParameters);
eControlWeekday getCurrentWeekday(void); void findControlCurrentTemperatureEntry(void);
sControlTemperatureEntry getCurrentTemperatureEntry(void); void setControlState(eControlState state);
void initControl(void) void initControl(void)
{ {
BaseType_t taskCreated = xTaskCreate(
taskControl, // Function to implement the task xMutexAccessControl = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessControl == NULL)
{
ESP_LOGE(TAG, "Unable to create mutex");
}
xSemaphoreGiveRecursive(xMutexAccessControl);
BaseType_t taskCreated =
xTaskCreate(taskControl, // Function to implement the task
"taskControl", // Task name "taskControl", // Task name
8192, // Stack size (in words, not bytes) 8192, // Stack size (in words, not bytes)
NULL, // Parameters to the task function (none in this case) NULL, // Parameters to the task function (none in this case)
@ -56,177 +113,293 @@ void initControl(void)
void taskControl(void *pvParameters) void taskControl(void *pvParameters)
{ {
bool bHeatingInAction = false; bool bHeatingInAction = false;
bool bBurnerFaultDetected = false; bool bSummerMode = false;
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
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 == true)
{
ESP_LOGW(TAG, "Control not possible due to safety fault: Disable burner");
bHeatingInAction = false;
setCirculationPumpState(ENABLED);
setBurnerState(DISABLED);
setSafetyControlState(ENABLED);
}
continue;
}
if (getSntpState() != SYNC_SUCCESSFUL)
{
ESP_LOGW(TAG, "Control not possible due to sntp fault!");
sControlState = CONTROL_FAULT_SNTP;
if (bHeatingInAction == true)
{
ESP_LOGW(TAG, "Control not possible due to sntp fault: Disable burner");
bHeatingInAction = false;
setCirculationPumpState(ENABLED);
setBurnerState(DISABLED);
setSafetyControlState(ENABLED);
}
continue;
}
sControlTemperatureEntry currentControlEntry = getCurrentTemperatureEntry();
// ESP_LOGI(TAG, "Control Entry Hour: %i Minute: %i ChamberTemp: %lf ReturnFlowTemp: %lf", currentControlEntry.timestamp.hour, currentControlEntry.timestamp.minute, currentControlEntry.fChamberTemperature, currentControlEntry.fReturnFlowTemperature);
if (bHeatingInAction == true)
{
if ((getChamberTemperature().fCurrentValue >= currentControlEntry.fChamberTemperature) || (getChamberTemperature().predict60s.fValue >= currentControlEntry.fChamberTemperature))
{
ESP_LOGI(TAG, "Chamber Target Temperature reached: Disable burner");
bHeatingInAction = false;
setCirculationPumpState(ENABLED);
setBurnerState(DISABLED);
setSafetyControlState(ENABLED);
}
else
{
if (bHeatingInAction) if (bHeatingInAction)
{ {
int64_t i64Delta = esp_timer_get_time() - i64BurnerEnableTimestamp; ESP_LOGW(TAG, "Disabling burner due to safety fault");
if ((i64Delta / 1000000U) >= BURNER_FAULT_DETECTION_THRESHOLD)
{
if (getBurnerError() == FAULT)
{
ESP_LOGW(TAG, "Detected burner fault after %lli seconds!", (i64Delta / 1000000U));
ESP_LOGW(TAG, "Control not possible due to burner fault: Disable burner");
sControlState = CONTROL_FAULT_BURNER;
bHeatingInAction = false; bHeatingInAction = false;
bBurnerFaultDetected = true;
setCirculationPumpState(ENABLED);
setBurnerState(DISABLED); setBurnerState(DISABLED);
setSafetyControlState(ENABLED); setSafetyControlState(ENABLED);
} }
} continue;
}
}
} }
if ((bHeatingInAction == false) && (bBurnerFaultDetected == false)) // Check for SNTP faults
if (getSntpState() != SYNC_SUCCESSFUL)
{ {
if ((getReturnFlowTemperature().average60s.fValue <= currentControlEntry.fReturnFlowTemperature) && (getChamberTemperature().fCurrentValue <= 45.0)) ESP_LOGW(TAG, "Control not possible due to SNTP fault!");
setControlState(CONTROL_FAULT_SNTP);
if (bHeatingInAction)
{ {
ESP_LOGI(TAG, "Return Flow Target Temperature reached: Enable Burner"); ESP_LOGW(TAG, "Disabling burner due to SNTP fault");
bHeatingInAction = false;
setBurnerState(DISABLED);
setSafetyControlState(ENABLED);
}
continue;
}
findControlCurrentTemperatureEntry();
if (getOutdoorTemperature().fDampedValue >=
SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH)
{
bSummerMode = true;
}
else if (getOutdoorTemperature().fDampedValue <=
SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW)
{
bSummerMode = false;
}
// Enable burner if outdoor temperature is low and return flow temperature
// is cooled down
if (!bHeatingInAction && (burnerState != BURNER_FAULT))
{
if (bSummerMode)
{
// ESP_LOGI(TAG, "Outdoor temperature too warm: Disabling heating");
setBurnerState(DISABLED);
setSafetyControlState(DISABLED);
setControlState(CONTROL_OUTDOOR_TOO_WARM);
}
else if ((getReturnFlowTemperature().average60s.fValue <=
getControlCurrentTemperatureEntry().fReturnFlowTemperature) &&
(getChamberTemperature().fCurrentValue <=
CHAMBER_TEMPERATURE_THRESHOLD))
{
ESP_LOGI(TAG,
"Enabling burner: Return flow temperature target reached");
burnerState = BURNER_UNKNOWN;
bHeatingInAction = true; bHeatingInAction = true;
setCirculationPumpState(ENABLED);
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
{ {
sControlState = CONTROL_RETURN_FLOW_TOO_WARM; // ESP_LOGI(TAG, "Return flow temperature too warm: Disabling heating");
setControlState(CONTROL_RETURN_FLOW_TOO_WARM);
} }
} }
// Disable burner if target temperature is reached or a fault occurred
if (bHeatingInAction)
{
if ((getChamberTemperature().fCurrentValue >=
getControlCurrentTemperatureEntry().fChamberTemperature) ||
(getChamberTemperature().predict60s.fValue >=
getControlCurrentTemperatureEntry().fChamberTemperature))
{
ESP_LOGI(TAG, "Chamber target temperature reached: Disabling burner");
bHeatingInAction = false;
setBurnerState(DISABLED);
setSafetyControlState(ENABLED);
}
else if (esp_timer_get_time() - i64BurnerEnableTimestamp >=
BURNER_FAULT_DETECTION_THRESHOLD * 1000000U)
{
if (burnerState == BURNER_UNKNOWN)
{
if (getBurnerError() == FAULT)
{
// ESP_LOGW(TAG, "Burner fault detected: Disabling burner");
bHeatingInAction = false;
burnerState = BURNER_FAULT;
setControlState(CONTROL_FAULT_BURNER);
setBurnerState(DISABLED);
setSafetyControlState(ENABLED);
}
else
{
// ESP_LOGI(TAG, "No burner fault detected: Marking burner as
// fired");
burnerState = BURNER_FIRED;
}
}
}
}
// Manage circulation pump
if (getChamberTemperature().fCurrentValue <=
CIRCULATION_PUMP_TEMPERATURE_THRESHOLD)
{
// ESP_LOGI(TAG, "Burner cooled down: Disabling circulation pump");
setCirculationPumpState(DISABLED);
}
else
{
// ESP_LOGI(TAG, "Burner heated: Enabling circulation pump");
setCirculationPumpState(ENABLED);
}
} // End of while(1)
}
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 getControlState(void)
{ {
return sControlState;
}
eControlWeekday getCurrentWeekday(void) eControlState ret = CONTROL_FAULT_SAFETY;
if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE)
{ {
time_t now; ret = gControlState;
struct tm *timeinfo; xSemaphoreGiveRecursive(xMutexAccessControl);
// Get the current time
time(&now);
timeinfo = localtime(&now); // Convert to local time
// Get the day of the week (0 = Sunday, 1 = Monday, ..., 6 = Saturday)
int day = timeinfo->tm_wday;
// Adjust so that Monday = 0, Sunday = 6
if (day == 0)
{
day = 6; // Sunday becomes 6
} }
else else
{ {
day -= 1; // Shift other days to make Monday = 0 ESP_LOGE(TAG, "Unable to take mutex: getControlState()");
} }
return (eControlWeekday)day; return ret;
} }
sControlTemperatureEntry getCurrentTemperatureEntry(void) eControlWeekday getControlCurrentWeekday(void)
{ {
sControlTemperatureEntry result = aControlTable[0].aTemperatureEntries[0]; // Get current time
eControlWeekday currentDay = getCurrentWeekday();
time_t now; time_t now;
struct tm timeinfo; struct tm timeinfo;
// Get the current time
time(&now); time(&now);
// Convert to local time structure
localtime_r(&now, &timeinfo); localtime_r(&now, &timeinfo);
// Extract hour and minute
int hour = timeinfo.tm_hour; // Hour (0-23)
int minute = timeinfo.tm_min; // Minute (0-59)u
// ESP_LOGI(TAG, "Current Day: %i Hour: %i Minute: %i", currentDay, hour, minute); int day = timeinfo.tm_wday;
return (eControlWeekday)((day == 0) ? 6 : day - 1);
}
for (int i = 0; i < sizeof(aControlTable) / sizeof(aControlTable[0]); i++) /**
* @brief Finds the active temperature control entry for the current time.
*
* Searches through the weekly schedule to find the most recent entry
* that should be active at the current date/time. Falls back to the
* last entry in the week if no suitable entry is found.
*/
/**
* @brief 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)
{ {
/// loops through days eControlWeekday currentDay = getControlCurrentWeekday();
// ESP_LOGI(TAG, "Day %d: %d", i + 1, aControlTable[i].day);
// int numberOfEntries = aControlTable[i].entryCount;
// ESP_LOGI(TAG, "Number of entries: %i", numberOfEntries);
for (int j = 0; j < aControlTable[i].entryCount; j++) // Get current time
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
int currentHour = timeinfo.tm_hour;
int currentMinute = timeinfo.tm_min;
if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE)
{ {
if ((aControlTable[i].day) > currentDay)
// ESP_LOGI(TAG, "Searching for control entry - Day: %d, Time: %02d:%02d", currentDay, currentHour, currentMinute);
// Search through all days and entries
for (int dayIndex = 0; dayIndex < 7; dayIndex++)
{ {
// ESP_LOGI(TAG, "DAY Return Control Entry Day: %i Hour: %i Minute: %i ChamberTemp: %lf ReturnFlowTemp: %lf", aControlTable[i].day, aControlTable[i].aTemperatureEntries[j].timestamp.hour, aControlTable[i].aTemperatureEntries[j].timestamp.minute, aControlTable[i].aTemperatureEntries[j].fChamberTemperature, aControlTable[i].aTemperatureEntries[j].fReturnFlowTemperature); const sControlDay *day = &gControlTable[dayIndex];
return result;
for (int entryIndex = 0; entryIndex < day->entryCount; entryIndex++)
{
const sControlTemperatureEntry *entry = &day->aTemperatureEntries[entryIndex];
// Check if this entry is in the future (next active entry)
bool isFutureDay = (day->day > currentDay);
bool isTodayFutureTime = (day->day == currentDay) &&
((entry->timestamp.hour > currentHour) ||
(entry->timestamp.hour == currentHour &&
entry->timestamp.minute > currentMinute));
if (isFutureDay || isTodayFutureTime)
{
// Found next scheduled entry, so determine the previous (active) one
if (entryIndex > 0)
{
// Use previous entry from same day
gCurrentControlEntry = day->aTemperatureEntries[entryIndex - 1];
}
else if (dayIndex > 0)
{
// Use last entry from previous day
const sControlDay *previousDay = &gControlTable[dayIndex - 1];
gCurrentControlEntry = previousDay->aTemperatureEntries[previousDay->entryCount - 1];
}
else
{
// First entry of the week - wrap to last entry of Sunday
const sControlDay *sunday = &gControlTable[6];
gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
}
/*
ESP_LOGI(TAG, "Active entry found - Time: %02d:%02d, "
"Return Temp: %lf, Chamber Temp: %lf",
gCurrentControlEntry.timestamp.hour,
gCurrentControlEntry.timestamp.minute,
gCurrentControlEntry.fReturnFlowTemperature,
gCurrentControlEntry.fChamberTemperature);
*/
return;
}
}
} }
if ((aControlTable[i].day == currentDay) && (aControlTable[i].aTemperatureEntries[j].timestamp.hour > hour)) // If we reached here, current time is after all entries this week
// Use the last entry (Sunday evening)
const sControlDay *sunday = &gControlTable[6];
gCurrentControlEntry = sunday->aTemperatureEntries[sunday->entryCount - 1];
// ESP_LOGI(TAG, "Using last entry of week - Time: %02d:%02d", gCurrentControlEntry.timestamp.hour, gCurrentControlEntry.timestamp.minute);
xSemaphoreGiveRecursive(xMutexAccessControl);
}
else
{ {
// ESP_LOGI(TAG, "HOUR Return Control Entry Day: %i Hour: %i Minute: %i ChamberTemp: %lf ReturnFlowTemp: %lf", aControlTable[i].day, aControlTable[i].aTemperatureEntries[j].timestamp.hour, aControlTable[i].aTemperatureEntries[j].timestamp.minute, aControlTable[i].aTemperatureEntries[j].fChamberTemperature, aControlTable[i].aTemperatureEntries[j].fReturnFlowTemperature); ESP_LOGE(TAG, "Unable to take mutex: findControlCurrentTemperatureEntry()");
return result; }
} }
if ((aControlTable[i].day == currentDay) && (aControlTable[i].aTemperatureEntries[j].timestamp.hour == hour) && (aControlTable[i].aTemperatureEntries[j].timestamp.minute == minute)) sControlTemperatureEntry getControlCurrentTemperatureEntry(void)
{ {
// ESP_LOGI(TAG, "MINUTE Return Control Entry Day: %i Hour: %i Minute: %i ChamberTemp: %lf ReturnFlowTemp: %lf", aControlTable[i].day, aControlTable[i].aTemperatureEntries[j].timestamp.hour, aControlTable[i].aTemperatureEntries[j].timestamp.minute, aControlTable[i].aTemperatureEntries[j].fChamberTemperature, aControlTable[i].aTemperatureEntries[j].fReturnFlowTemperature); sControlTemperatureEntry ret = gControlTable[0].aTemperatureEntries[0];
return result; if (xSemaphoreTakeRecursive(xMutexAccessControl, pdMS_TO_TICKS(5000)) == pdTRUE)
{
ret = gCurrentControlEntry;
xSemaphoreGiveRecursive(xMutexAccessControl);
}
else
{
ESP_LOGE(TAG, "Unable to take mutex: getControlCurrentTemperatureEntry()");
} }
// ESP_LOGI(TAG, "SET Return Control Entry Day: %i Hour: %i Minute: %i ChamberTemp: %lf ReturnFlowTemp: %lf", aControlTable[i].day, aControlTable[i].aTemperatureEntries[j].timestamp.hour, aControlTable[i].aTemperatureEntries[j].timestamp.minute, aControlTable[i].aTemperatureEntries[j].fChamberTemperature, aControlTable[i].aTemperatureEntries[j].fReturnFlowTemperature); return ret;
result = aControlTable[i].aTemperatureEntries[j];
}
}
return result;
} }

View File

@ -1,8 +1,20 @@
#pragma once #pragma once
#include "sdkconfig.h"
#include <time.h> #include <time.h>
#define MAX_TEMPERATURE_ENTRIES_PER_DAY 24U #define MAX_TEMPERATURE_ENTRIES_PER_DAY 24U
#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_DAY (CONFIG_TEMP_RETURN_FLOW_LOWER_LIMIT_DAY / 10.0f)
#define RETURN_FLOW_TEMPERATURE_LOWER_LIMIT_NIGHT (CONFIG_TEMP_RETURN_FLOW_LOWER_LIMIT_NIGHT / 10.0f)
#define CHAMBER_TEMPERATURE_TARGET (CONFIG_TEMP_CHAMBER_TARGET / 10.0f)
#define CHAMBER_TEMPERATURE_THRESHOLD (CONFIG_TEMP_CHAMBER_THRESHOLD / 10.0f)
#define SUMMER_MODE_TEMPERATURE_THRESHOLD_HIGH (CONFIG_TEMP_SUMMER_MODE_HIGH / 10.0f)
#define SUMMER_MODE_TEMPERATURE_THRESHOLD_LOW (CONFIG_TEMP_SUMMER_MODE_LOW / 10.0f)
#define CIRCULATION_PUMP_TEMPERATURE_THRESHOLD (CONFIG_TEMP_CIRCULATION_PUMP_THRESHOLD / 10.0f)
#define BURNER_FAULT_DETECTION_THRESHOLD CONFIG_BURNER_FAULT_DETECTION_SECONDS
typedef enum _ControlState typedef enum _ControlState
{ {
CONTROL_STARTING, CONTROL_STARTING,
@ -14,6 +26,13 @@ typedef enum _ControlState
CONTROL_FAULT_SNTP, CONTROL_FAULT_SNTP,
} eControlState; } eControlState;
typedef enum _BurnerState
{
BURNER_UNKNOWN, // Burner is disabled or state after enabling is still unkown
BURNER_FIRED, // Burner fired successfully
BURNER_FAULT // Burner was unable to fire successfully
} eBurnerState;
typedef enum _ControlWeekday typedef enum _ControlWeekday
{ {
MONDAY, MONDAY,
@ -47,3 +66,5 @@ typedef struct _ControlDay
void initControl(void); void initControl(void);
eControlState getControlState(void); eControlState getControlState(void);
eControlWeekday getControlCurrentWeekday(void);
sControlTemperatureEntry getControlCurrentTemperatureEntry(void);

View File

@ -1,25 +1,26 @@
#include "inputs.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#include <string.h>
#include <math.h>
#include "esp_log.h" #include "esp_log.h"
#include <ds18x20.h> #include <ds18x20.h>
#include "inputs.h" #include <string.h>
#include <math.h>
#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 ONE_WIRE_LOOPS 4U // try to read the 1-Wire sensors that often
#define PERIODIC_INTERVAL 1U // read and compute the inputs every 1sec #define PERIODIC_INTERVAL 1U // read and compute the inputs every 1sec
static const char *TAG = "smart-oil-heater-control-system-inputs"; static const char *TAG = "smart-oil-heater-control-system-inputs";
const uint8_t uBurnerFaultPin = 19U; const uint8_t uBurnerFaultPin = CONFIG_GPIO_BURNER_FAULT;
const uint8_t uDS18B20Pin = 4U; const uint8_t uDS18B20Pin = CONFIG_GPIO_DS18B20_ONEWIRE;
const onewire_addr_t uChamperTempSensorAddr = 0xd00000108cd01d28; const onewire_addr_t uChamperTempSensorAddr = CONFIG_ONEWIRE_ADDR_CHAMBER_TEMP;
const onewire_addr_t uOutdoorTempSensorAddr = 0x78000000c6c2f728; const onewire_addr_t uOutdoorTempSensorAddr = CONFIG_ONEWIRE_ADDR_OUTDOOR_TEMP;
const onewire_addr_t uInletFlowTempSensorAddr = 0x410000108b8c0628; const onewire_addr_t uInletFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_INLET_FLOW_TEMP;
const onewire_addr_t uReturnFlowTempSensorAddr = 0x90000108cc77c28; const onewire_addr_t uReturnFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_RETURN_FLOW_TEMP;
onewire_addr_t uOneWireAddresses[MAX_DN18B20_SENSORS]; onewire_addr_t uOneWireAddresses[MAX_DN18B20_SENSORS];
float fDS18B20Temps[MAX_DN18B20_SENSORS]; float fDS18B20Temps[MAX_DN18B20_SENSORS];
@ -49,7 +50,12 @@ void initInputs(void)
.intr_type = GPIO_INTR_DISABLE // Disable interrupts .intr_type = GPIO_INTR_DISABLE // Disable interrupts
}; };
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;
}
xMutexAccessInputs = xSemaphoreCreateRecursiveMutex(); xMutexAccessInputs = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessInputs == NULL) if (xMutexAccessInputs == NULL)
@ -88,22 +94,23 @@ void initMeasurement(sMeasurement *pMeasurement)
return; return;
pMeasurement->state = MEASUREMENT_FAULT; pMeasurement->state = MEASUREMENT_FAULT;
pMeasurement->fCurrentValue = 0.0f; pMeasurement->fCurrentValue = INITIALISATION_VALUE;
pMeasurement->fDampedValue = INITIALISATION_VALUE;
pMeasurement->average10s.fValue = 0.0f; 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, AVG10_SAMPLE_SIZE); memset(pMeasurement->average10s.samples, 0U, sizeof(float) * AVG10S_SAMPLE_SIZE);
pMeasurement->average60s.fValue = 0.0f; 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, AVG60_SAMPLE_SIZE); memset(pMeasurement->average60s.samples, 0U, sizeof(float) * AVG60S_SAMPLE_SIZE);
pMeasurement->predict60s.fValue = 0.0f; 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, PRED60_SAMPLE_SIZE); memset(pMeasurement->predict60s.samples, 0U, sizeof(float) * PRED60S_SAMPLE_SIZE);
} }
void updateAverage(sMeasurement *pMeasurement) void updateAverage(sMeasurement *pMeasurement)
@ -113,26 +120,33 @@ void updateAverage(sMeasurement *pMeasurement)
// Average form the last 10sec // Average form the last 10sec
pMeasurement->average10s.samples[pMeasurement->average10s.bufferIndex] = pMeasurement->fCurrentValue; pMeasurement->average10s.samples[pMeasurement->average10s.bufferIndex] = pMeasurement->fCurrentValue;
pMeasurement->average10s.bufferIndex = (pMeasurement->average10s.bufferIndex + 1) % AVG10_SAMPLE_SIZE; pMeasurement->average10s.bufferIndex = (pMeasurement->average10s.bufferIndex + 1) % AVG10S_SAMPLE_SIZE;
if (pMeasurement->average10s.bufferCount < AVG10_SAMPLE_SIZE) if (pMeasurement->average10s.bufferCount < AVG10S_SAMPLE_SIZE)
{ {
pMeasurement->average10s.bufferCount++; pMeasurement->average10s.bufferCount++;
} }
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 // Average form the last 60sec
pMeasurement->average60s.samples[pMeasurement->average60s.bufferIndex] = pMeasurement->fCurrentValue; pMeasurement->average60s.samples[pMeasurement->average60s.bufferIndex] = pMeasurement->fCurrentValue;
pMeasurement->average60s.bufferIndex = (pMeasurement->average60s.bufferIndex + 1) % AVG60_SAMPLE_SIZE; pMeasurement->average60s.bufferIndex = (pMeasurement->average60s.bufferIndex + 1) % AVG60S_SAMPLE_SIZE;
if (pMeasurement->average60s.bufferCount < AVG60_SAMPLE_SIZE) if (pMeasurement->average60s.bufferCount < AVG60S_SAMPLE_SIZE)
{ {
pMeasurement->average60s.bufferCount++; pMeasurement->average60s.bufferCount++;
} }
@ -143,9 +157,34 @@ 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
if (pMeasurement->fDampedValue == INITIALISATION_VALUE)
{
pMeasurement->fDampedValue = pMeasurement->fCurrentValue;
}
else
{
if (pMeasurement->fCurrentValue > pMeasurement->fDampedValue)
{
pMeasurement->fDampedValue = pMeasurement->fDampedValue + (DAMPING_FACTOR_WARMER * (pMeasurement->fCurrentValue - pMeasurement->fDampedValue));
}
if (pMeasurement->fCurrentValue < pMeasurement->fDampedValue)
{
pMeasurement->fDampedValue = pMeasurement->fDampedValue - (DAMPING_FACTOR_COLDER * (pMeasurement->fDampedValue - pMeasurement->fCurrentValue));
}
}
}
void updatePrediction(sMeasurement *pMeasurement) void updatePrediction(sMeasurement *pMeasurement)
{ {
if (!pMeasurement) if (!pMeasurement)
@ -154,8 +193,8 @@ void updatePrediction(sMeasurement *pMeasurement)
// Update predict60s buffer // Update predict60s buffer
sPredict *predict60s = &pMeasurement->predict60s; sPredict *predict60s = &pMeasurement->predict60s;
predict60s->samples[predict60s->bufferIndex] = pMeasurement->fCurrentValue; predict60s->samples[predict60s->bufferIndex] = pMeasurement->fCurrentValue;
predict60s->bufferIndex = (predict60s->bufferIndex + 1) % PRED60_SAMPLE_SIZE; predict60s->bufferIndex = (predict60s->bufferIndex + 1) % PRED60S_SAMPLE_SIZE;
if (predict60s->bufferCount < PRED60_SAMPLE_SIZE) if (predict60s->bufferCount < PRED60S_SAMPLE_SIZE)
predict60s->bufferCount++; predict60s->bufferCount++;
// Predict 60s future value using linear regression // Predict 60s future value using linear regression
@ -271,9 +310,9 @@ void taskInput(void *pvParameters)
float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex) float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex)
{ {
if (count == 0) if (count == 0)
return 0.0f; // No prediction possible with no data return INITIALISATION_VALUE; // No prediction possible with no data
float sumX = 0.0f, sumY = 0.0f, sumXY = 0.0f, sumX2 = 0.0f; 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++)
{ {

View File

@ -1,9 +1,17 @@
#pragma once #pragma once
#include "sdkconfig.h"
#include <stddef.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MAX(a, b) ((a) > (b) ? (a) : (b))
#define AVG10_SAMPLE_SIZE 10U #define INITIALISATION_VALUE 0.0f
#define AVG60_SAMPLE_SIZE 60U #define AVG10S_SAMPLE_SIZE 10U
#define PRED60_SAMPLE_SIZE 60U #define AVG60S_SAMPLE_SIZE 60U
#define AVG24H_SAMPLE_SIZE 24U
#define PRED60S_SAMPLE_SIZE 60U
#define DAMPING_FACTOR_WARMER (CONFIG_DAMPING_FACTOR_WARMER * 0.00001f)
#define DAMPING_FACTOR_COLDER (CONFIG_DAMPING_FACTOR_COLDER * 0.00001f)
typedef enum _BurnerErrorState typedef enum _BurnerErrorState
{ {
@ -20,7 +28,7 @@ typedef enum _MeasurementErrorState
typedef struct _Average typedef struct _Average
{ {
float fValue; float fValue;
float samples[MAX(AVG10_SAMPLE_SIZE, AVG60_SAMPLE_SIZE)]; float samples[MAX(AVG10S_SAMPLE_SIZE, MAX(AVG60S_SAMPLE_SIZE, AVG24H_SAMPLE_SIZE))];
size_t bufferIndex; size_t bufferIndex;
size_t bufferCount; size_t bufferCount;
} sAverage; } sAverage;
@ -28,7 +36,7 @@ typedef struct _Average
typedef struct _Predict typedef struct _Predict
{ {
float fValue; float fValue;
float samples[PRED60_SAMPLE_SIZE]; float samples[PRED60S_SAMPLE_SIZE];
size_t bufferIndex; size_t bufferIndex;
size_t bufferCount; size_t bufferCount;
} sPredict; } sPredict;
@ -36,6 +44,7 @@ typedef struct _Predict
typedef struct _Measurement typedef struct _Measurement
{ {
float fCurrentValue; float fCurrentValue;
float fDampedValue;
sAverage average10s; sAverage average10s;
sAverage average60s; sAverage average60s;
sPredict predict60s; sPredict predict60s;

View File

@ -1,7 +1,3 @@
#include "esp_log.h"
#include <esp_system.h>
#include "nvs_flash.h"
#include "safety.h" #include "safety.h"
#include "metrics.h" #include "metrics.h"
#include "outputs.h" #include "outputs.h"
@ -10,6 +6,10 @@
#include "wifi.h" #include "wifi.h"
#include "sntp.h" #include "sntp.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
static const char *TAG = "smart-oil-heater-control-system"; static const char *TAG = "smart-oil-heater-control-system";
void app_main(void) void app_main(void)

View File

@ -1,12 +1,3 @@
#include <string.h>
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include <time.h>
#include <sys/time.h>
#include "metrics.h" #include "metrics.h"
#include "outputs.h" #include "outputs.h"
#include "inputs.h" #include "inputs.h"
@ -14,6 +5,16 @@
#include "sntp.h" #include "sntp.h"
#include "control.h" #include "control.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include <string.h>
#include <time.h>
#include <sys/time.h>
static const char *TAG = "smart-oil-heater-control-system-metrics"; static const char *TAG = "smart-oil-heater-control-system-metrics";
char caHtmlResponse[HTML_RESPONSE_SIZE]; char caHtmlResponse[HTML_RESPONSE_SIZE];
@ -128,6 +129,12 @@ void taskMetrics(void *pvParameters)
aMetrics[u16MetricCounter].fMetricValue = getChamberTemperature().average60s.fValue; aMetrics[u16MetricCounter].fMetricValue = getChamberTemperature().average60s.fValue;
u16MetricCounter++; u16MetricCounter++;
// Chamber Temperature Damped
strcpy(aMetrics[u16MetricCounter].caMetricName, "chamber_temperature_damped");
aMetrics[u16MetricCounter].type = FLOAT;
aMetrics[u16MetricCounter].fMetricValue = getChamberTemperature().fDampedValue;
u16MetricCounter++;
// Chamber Temperature Predict 60s // Chamber Temperature Predict 60s
strcpy(aMetrics[u16MetricCounter].caMetricName, "chamber_temperature_pred60"); strcpy(aMetrics[u16MetricCounter].caMetricName, "chamber_temperature_pred60");
aMetrics[u16MetricCounter].type = FLOAT; aMetrics[u16MetricCounter].type = FLOAT;
@ -152,6 +159,12 @@ void taskMetrics(void *pvParameters)
aMetrics[u16MetricCounter].fMetricValue = getInletFlowTemperature().average60s.fValue; aMetrics[u16MetricCounter].fMetricValue = getInletFlowTemperature().average60s.fValue;
u16MetricCounter++; u16MetricCounter++;
// Inlet Flow Temperature Damped
strcpy(aMetrics[u16MetricCounter].caMetricName, "inlet_flow_temperature_damped");
aMetrics[u16MetricCounter].type = FLOAT;
aMetrics[u16MetricCounter].fMetricValue = getInletFlowTemperature().fDampedValue;
u16MetricCounter++;
// Inlet Flow Temperature Predict 60s // Inlet Flow Temperature Predict 60s
strcpy(aMetrics[u16MetricCounter].caMetricName, "inlet_flow_temperature_pred60"); strcpy(aMetrics[u16MetricCounter].caMetricName, "inlet_flow_temperature_pred60");
aMetrics[u16MetricCounter].type = FLOAT; aMetrics[u16MetricCounter].type = FLOAT;
@ -176,6 +189,12 @@ void taskMetrics(void *pvParameters)
aMetrics[u16MetricCounter].fMetricValue = getOutdoorTemperature().average60s.fValue; aMetrics[u16MetricCounter].fMetricValue = getOutdoorTemperature().average60s.fValue;
u16MetricCounter++; u16MetricCounter++;
// Outdoor Temperature Average Damped
strcpy(aMetrics[u16MetricCounter].caMetricName, "outdoor_temperature_damped");
aMetrics[u16MetricCounter].type = FLOAT;
aMetrics[u16MetricCounter].fMetricValue = getOutdoorTemperature().fDampedValue;
u16MetricCounter++;
// Outdoor Temperature Predict 60s // Outdoor Temperature Predict 60s
strcpy(aMetrics[u16MetricCounter].caMetricName, "outdoor_temperature_pred60"); strcpy(aMetrics[u16MetricCounter].caMetricName, "outdoor_temperature_pred60");
aMetrics[u16MetricCounter].type = FLOAT; aMetrics[u16MetricCounter].type = FLOAT;
@ -200,6 +219,12 @@ void taskMetrics(void *pvParameters)
aMetrics[u16MetricCounter].fMetricValue = getReturnFlowTemperature().average60s.fValue; aMetrics[u16MetricCounter].fMetricValue = getReturnFlowTemperature().average60s.fValue;
u16MetricCounter++; u16MetricCounter++;
// Return Flow Temperature Damped
strcpy(aMetrics[u16MetricCounter].caMetricName, "return_flow_temperature_damped");
aMetrics[u16MetricCounter].type = FLOAT;
aMetrics[u16MetricCounter].fMetricValue = getReturnFlowTemperature().fDampedValue;
u16MetricCounter++;
// Return Flow Temperature Predict 60s // Return Flow Temperature Predict 60s
strcpy(aMetrics[u16MetricCounter].caMetricName, "return_flow_temperature_pred60"); strcpy(aMetrics[u16MetricCounter].caMetricName, "return_flow_temperature_pred60");
aMetrics[u16MetricCounter].type = FLOAT; aMetrics[u16MetricCounter].type = FLOAT;
@ -230,6 +255,31 @@ void taskMetrics(void *pvParameters)
aMetrics[u16MetricCounter].u8MetricValue = getControlState(); aMetrics[u16MetricCounter].u8MetricValue = getControlState();
u16MetricCounter++; u16MetricCounter++;
// Control Current Weekday
strcpy(aMetrics[u16MetricCounter].caMetricName, "control_current_weekday");
aMetrics[u16MetricCounter].type = INTEGER_U8;
aMetrics[u16MetricCounter].u8MetricValue = getControlCurrentWeekday();
u16MetricCounter++;
// Control Current Entry Time
strcpy(aMetrics[u16MetricCounter].caMetricName, "control_current_entry_time");
aMetrics[u16MetricCounter].type = INTEGER_64;
int64_t i64SecondsSinceMidnight = (getControlCurrentTemperatureEntry().timestamp.hour * 60U * 60U) + (getControlCurrentTemperatureEntry().timestamp.minute * 60U);
aMetrics[u16MetricCounter].i64MetricValue = i64SecondsSinceMidnight;
u16MetricCounter++;
// Control Current Entry Chamber Temperature
strcpy(aMetrics[u16MetricCounter].caMetricName, "control_current_entry_chamber_temperature");
aMetrics[u16MetricCounter].type = FLOAT;
aMetrics[u16MetricCounter].fMetricValue = getControlCurrentTemperatureEntry().fChamberTemperature;
u16MetricCounter++;
// Control Current Entry Return Flow Temperature
strcpy(aMetrics[u16MetricCounter].caMetricName, "control_current_entry_return_flow_temperature");
aMetrics[u16MetricCounter].type = FLOAT;
aMetrics[u16MetricCounter].fMetricValue = getControlCurrentTemperatureEntry().fReturnFlowTemperature;
u16MetricCounter++;
// SNTP State // SNTP State
strcpy(aMetrics[u16MetricCounter].caMetricName, "sntp_state"); strcpy(aMetrics[u16MetricCounter].caMetricName, "sntp_state");
aMetrics[u16MetricCounter].type = INTEGER_U8; aMetrics[u16MetricCounter].type = INTEGER_U8;
@ -252,22 +302,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++;
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];

View File

@ -4,7 +4,7 @@
#define HTML_RESPONSE_SIZE 4096U #define HTML_RESPONSE_SIZE 4096U
#define METRIC_NAME_MAX_SIZE 64U #define METRIC_NAME_MAX_SIZE 64U
#define METRIC_MAX_COUNT 32U #define METRIC_MAX_COUNT 38U
typedef enum _MetricValueType typedef enum _MetricValueType
{ {

View File

@ -1,14 +1,15 @@
#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 = "smart-oil-heater-control-system-outputs"; static const char *TAG = "smart-oil-heater-control-system-outputs";
const uint8_t uCirculationPumpGpioPin = 27U; const uint8_t uCirculationPumpGpioPin = CONFIG_GPIO_CIRCULATION_PUMP;
const uint8_t uBurnerGpioPin = 14U; const uint8_t uBurnerGpioPin = CONFIG_GPIO_BURNER;
const uint8_t uSafetyContactGpioPin = 12U; const uint8_t uSafetyContactGpioPin = CONFIG_GPIO_SAFETY_CONTACT;
static SemaphoreHandle_t xMutexAccessOutputs = NULL; static SemaphoreHandle_t xMutexAccessOutputs = NULL;
static eOutput sCirculationPumpState; static eOutput sCirculationPumpState;
@ -41,9 +42,26 @@ void initOutputs(void)
.intr_type = GPIO_INTR_DISABLE // Disable interrupts .intr_type = GPIO_INTR_DISABLE // Disable interrupts
}; };
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: %s", esp_err_to_name(ret));
return;
}
ret = gpio_config(&ioConfBurner);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret));
return;
}
ret = gpio_config(&ioConfSafetyContact);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret));
return;
}
xMutexAccessOutputs = xSemaphoreCreateRecursiveMutex(); xMutexAccessOutputs = xSemaphoreCreateRecursiveMutex();
if (xMutexAccessOutputs == NULL) if (xMutexAccessOutputs == NULL)
@ -55,7 +73,17 @@ void initOutputs(void)
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 +98,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 +137,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 +176,7 @@ void setSafetyControlState(eOutput in)
break; break;
case DISABLED: case DISABLED:
gpio_set_level(uSafetyContactGpioPin, 1U); // Switch off power for Burner gpio_set_level(uSafetyContactGpioPin, 1U); // Switch off power for Burner
break;
default: default:
break; break;
} }

View File

@ -1,19 +1,22 @@
#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 #define PERIODIC_INTERVAL 1U // run safety checks every 1sec
#define SENSOR_GRACE_PERIOD (60U * 30U) // period that a sensor can report the same reading in seconds #define SENSOR_GRACE_PERIOD (CONFIG_SENSOR_GRACE_PERIOD_MINUTES * 60U) // period that a sensor can report the same reading in seconds
#define FLOAT_EPSILON 0.0001f
static const char *TAG = "smart-oil-heater-control-system-safety"; static const char *TAG = "smart-oil-heater-control-system-safety";
static SemaphoreHandle_t xMutexAccessSafety = NULL; static SemaphoreHandle_t xMutexAccessSafety = NULL;
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); void taskSafety(void *pvParameters);
@ -91,7 +94,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 +106,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,12 +123,10 @@ 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);
} }
} }
@ -143,7 +145,7 @@ void getSensorSanityStates(sSensorSanityCheck *pSensorSanityChecks)
{ {
// Copy only the needed attributes // Copy only the needed attributes
pSensorSanityChecks[i].state = sanityChecks[i].state; pSensorSanityChecks[i].state = sanityChecks[i].state;
strcpy(pSensorSanityChecks[i].name, sanityChecks[i].name); strncpy(pSensorSanityChecks[i].name, sanityChecks[i].name, MAX_ERROR_STRING_SIZE);
} }
xSemaphoreGiveRecursive(xMutexAccessSafety); xSemaphoreGiveRecursive(xMutexAccessSafety);
} }

View File

@ -3,9 +3,22 @@
#include "outputs.h" #include "outputs.h"
#include "inputs.h" #include "inputs.h"
#include "sdkconfig.h"
#include <stdint.h>
#define MAX_ERROR_STRING_SIZE 64U #define MAX_ERROR_STRING_SIZE 64U
#define NUMBER_OF_SENSOR_SANITY_CHECKS 4U #define NUMBER_OF_SENSOR_SANITY_CHECKS 4U
#define SENSOR_LIMIT_CHAMBER_MAX (CONFIG_SENSOR_LIMIT_CHAMBER_MAX / 10.0f)
#define SENSOR_LIMIT_CHAMBER_MIN (CONFIG_SENSOR_LIMIT_CHAMBER_MIN / 10.0f)
#define SENSOR_LIMIT_OUTDOOR_MAX (CONFIG_SENSOR_LIMIT_OUTDOOR_MAX / 10.0f)
#define SENSOR_LIMIT_OUTDOOR_MIN (CONFIG_SENSOR_LIMIT_OUTDOOR_MIN / 10.0f)
#define SENSOR_LIMIT_INLET_MAX (CONFIG_SENSOR_LIMIT_INLET_MAX / 10.0f)
#define SENSOR_LIMIT_INLET_MIN (CONFIG_SENSOR_LIMIT_INLET_MIN / 10.0f)
#define SENSOR_LIMIT_RETURN_MAX (CONFIG_SENSOR_LIMIT_RETURN_MAX / 10.0f)
#define SENSOR_LIMIT_RETURN_MIN (CONFIG_SENSOR_LIMIT_RETURN_MIN / 10.0f)
typedef enum _SensorErrorState typedef enum _SensorErrorState
{ {
SENSOR_NO_ERROR, SENSOR_NO_ERROR,

View File

@ -1,12 +1,13 @@
#include <time.h>
#include <sys/time.h>
#include <esp_sntp.h>
#include "esp_log.h"
#include "sntp.h" #include "sntp.h"
#include "esp_sntp.h"
#include "esp_log.h"
#include <time.h>
#include <sys/time.h>
static const char *TAG = "smart-oil-heater-control-system-sntp"; static const char *TAG = "smart-oil-heater-control-system-sntp";
static eSntpState sntpState = SYNC_NOT_STARTED; static volatile eSntpState sntpState = SYNC_NOT_STARTED;
void time_sync_notification_cb(struct timeval *tv); void time_sync_notification_cb(struct timeval *tv);
void initSntp(void) void initSntp(void)

View File

@ -1,38 +1,50 @@
#include <string.h> #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>
#define WIFI_CONNECTED_BIT BIT0 #define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1 #define WIFI_FAIL_BIT BIT1
#define MAX_RETRY_COUNT 10
#define RETRY_DELAY_MS 1000
static const char *TAG = "smart-oil-heater-control-system-wifi"; static const char *TAG = "smart-oil-heater-control-system-wifi";
static EventGroupHandle_t s_wifi_event_group; static EventGroupHandle_t s_wifi_event_group;
static int s_retry_num = 0;
static bool s_initial_connect = true;
static void event_handler(void *arg, esp_event_base_t event_base, static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data); int32_t event_id, void *event_data);
void initWifi(void) void initWifi(void)
{ {
s_wifi_event_group = xEventGroupCreate(); s_wifi_event_group = xEventGroupCreate();
if (s_wifi_event_group == NULL)
{
ESP_LOGE(TAG, "xEventGroupCreate() failed!");
return;
}
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));
@ -84,7 +96,9 @@ void initWifi(void)
{ {
ESP_LOGE(TAG, "Unexpected event"); ESP_LOGE(TAG, "Unexpected event");
} }
vEventGroupDelete(s_wifi_event_group);
// Mark initial connection phase complete - do NOT delete the event group
s_initial_connect = false;
} }
static void event_handler(void *arg, esp_event_base_t event_base, static void event_handler(void *arg, esp_event_base_t event_base,
@ -96,13 +110,46 @@ static void event_handler(void *arg, esp_event_base_t event_base,
} }
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{ {
wifi_event_sta_disconnected_t *event = (wifi_event_sta_disconnected_t *)event_data;
ESP_LOGW(TAG, "Disconnected from AP (reason: %d)", event->reason);
if (s_initial_connect)
{
// During initial connection phase, use retry limit
if (s_retry_num < MAX_RETRY_COUNT)
{
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
esp_wifi_connect(); esp_wifi_connect();
ESP_LOGI(TAG, "Retry to connect to the AP"); s_retry_num++;
ESP_LOGI(TAG, "Retry to connect to the AP (%d/%d)", s_retry_num, MAX_RETRY_COUNT);
}
else
{
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
ESP_LOGE(TAG, "Failed to connect after %d attempts", MAX_RETRY_COUNT);
}
}
else
{
// After initial connection, always try to reconnect with delay
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
esp_wifi_connect();
ESP_LOGI(TAG, "Attempting to reconnect to the AP...");
}
} }
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{ {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip)); ESP_LOGI(TAG, "Got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
if (s_initial_connect)
{
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
} }
else
{
ESP_LOGI(TAG, "Successfully reconnected to AP");
}
}
} }