ESP32 firmware to control a thermal solar installation
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

299 lines
13 KiB

#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "solar.h"
#include "mqtt.h"
#include "common.h"
// 1-Wire support
#include "owb.h"
#include "owb_rmt.h"
#include "ds18b20.h"
// PWM support
#include "driver/gpio.h"
#include "driver/ledc.h"
#define SC_MAX_TEMPERATURE_SENSORS_COUNT 8
#define SC_EXPECTED_TEMPERATURE_SENSORS_COUNT 4
#define SC_PUMP_FULL_POWER 255
#define SC_PUMP_HALF_POWER 150
#define SC_PUMP_NO_POWER 0
#define SC_LEDC_CHANNEL_PANEL_PUMP LEDC_CHANNEL_0
#define SC_LEDC_CHANNEL_FLOOR_HEATING_PUMP LEDC_CHANNEL_1
static const char *SOLAR_LOGGER = "solar";
static DS18B20_Info temperature_sensors[SC_EXPECTED_TEMPERATURE_SENSORS_COUNT];
static int panel;
static int store_high;
static int store_low;
static int floor_heating;
static OneWireBus * owb;
static owb_rmt_driver_info rmt_driver_info;
static int is_heating_enabled = 0;
void solar_set_heating(int enabled) {
is_heating_enabled = enabled;
if (IS_READY(MQTT_CONNECTED_BIT|TIME_SYNC_BIT)) {
mqtt_publish_data("floor_heating_enabled", (json_value){MQTT_TYPE_INT, {.i = is_heating_enabled}});
}
}
static int map_pwm(float delta_temp) {
float res = delta_temp * 10.2;
if (res > 255) {
return 255;
}
return res;
}
static void solar_process(void *pvParameters) {
TickType_t last_wake_time = xTaskGetTickCount();
int is_protecting_from_frost = 0;
int is_loading = 0;
int is_heating = 0;
int panel_pump_pwm = 0;
int floor_heating_pump_pwm = 0;
int errors_count[SC_EXPECTED_TEMPERATURE_SENSORS_COUNT] = {0};
for (;;) {
float readings[SC_EXPECTED_TEMPERATURE_SENSORS_COUNT];
DS18B20_ERROR errors[SC_EXPECTED_TEMPERATURE_SENSORS_COUNT] = { 0 };
// Read temperatures more efficiently by starting conversions on all devices at the same time
ds18b20_convert_all(owb);
// In this application all devices use the same resolution,
// so use the first device to determine the delay
ds18b20_wait_for_conversion(&temperature_sensors[0]);
// Read the results immediately after conversion otherwise it may fail
// (using printf before reading may take too long)
for (int i = 0; i < SC_EXPECTED_TEMPERATURE_SENSORS_COUNT; ++i) {
errors[i] = ds18b20_read_temp(&temperature_sensors[i], &readings[i]);
}
// Check if there are errors during measure or data transmission
int has_reading_errors = 0;
for (int i = 0; i < SC_EXPECTED_TEMPERATURE_SENSORS_COUNT; ++i) {
if (errors[i] != DS18B20_OK) {
char rom_code_s[17];
owb_string_from_rom_code(temperature_sensors[i].rom_code, rom_code_s, sizeof(rom_code_s));
ESP_LOGW(SOLAR_LOGGER, "Error reading sensor %s.", rom_code_s);
++errors_count[i];
has_reading_errors = 1;
}
}
// And retry a measure if there were errors
if (has_reading_errors) {
ESP_LOGW(SOLAR_LOGGER, "There were errors while reading sensors. Retrying!");
continue;
}
float delta_panel = readings[panel] - readings[store_low];
float delta_floor_heating = readings[store_high] - readings[floor_heating];
if (!is_protecting_from_frost && readings[panel] < CONFIG_SC_FROST_PROTECTION_TEMP_LOW) {
ESP_LOGI(SOLAR_LOGGER, "Temperature is running low in the panel (%.1f), engaging frost protection!", readings[panel]);
is_protecting_from_frost = 1;
panel_pump_pwm = SC_PUMP_HALF_POWER;
} else if (is_protecting_from_frost && readings[panel] > CONFIG_SC_FROST_PROTECTION_TEMP_HIGH) {
ESP_LOGI(SOLAR_LOGGER, "Disengaging frost protection...");
is_protecting_from_frost = 0;
panel_pump_pwm = SC_PUMP_NO_POWER;
} else if (!is_loading && delta_panel > CONFIG_SC_PANEL_DELTA_TEMP_HIGH) {
ESP_LOGI(SOLAR_LOGGER, "Start loading heat from the panel to the store...");
panel_pump_pwm = map_pwm(delta_panel);
is_loading = 1;
} else if (is_loading && delta_panel < CONFIG_SC_PANEL_DELTA_TEMP_LOW) {
ESP_LOGI(SOLAR_LOGGER, "Stop loading heat from the panel to the store...");
panel_pump_pwm = SC_PUMP_NO_POWER;
is_loading = 0;
} else if (is_loading) {
// Keep adjusting the pump duty cycle according to the temperature difference
panel_pump_pwm = map_pwm(delta_panel);
}
if (is_heating && !is_heating_enabled) {
ESP_LOGI(SOLAR_LOGGER, "Stop heating the floor (as requested).");
floor_heating_pump_pwm = SC_PUMP_NO_POWER;
is_heating = 0;
} else if (is_heating_enabled && !is_heating && delta_floor_heating > CONFIG_SC_FLOOR_HEATING_DELTA_TEMP_HIGH) {
ESP_LOGI(SOLAR_LOGGER, "Start heating the floor from the store...");
floor_heating_pump_pwm = SC_PUMP_HALF_POWER;
is_heating = 1;
} else if (is_heating && delta_floor_heating < CONFIG_SC_FLOOR_HEATING_DELTA_TEMP_LOW) {
ESP_LOGI(SOLAR_LOGGER, "Stop heating the floor from the store...");
floor_heating_pump_pwm = SC_PUMP_NO_POWER;
is_heating = 0;
}
ESP_LOGI(SOLAR_LOGGER, "p:%2.1f°C, s.l:%2.1f°C, s.h:%2.1f°C, f.h:%2.1f°C, p.p: %3.0f%%, fh.p: %3.0f%%", readings[panel], readings[store_low], readings[store_high], readings[floor_heating], panel_pump_pwm / 2.55, floor_heating_pump_pwm / 2.55);
// Set new PWM duty cycle on each pump
ESP_ERROR_CHECK(ledc_set_duty(LEDC_HIGH_SPEED_MODE, SC_LEDC_CHANNEL_PANEL_PUMP, panel_pump_pwm));
ESP_ERROR_CHECK(ledc_set_duty(LEDC_HIGH_SPEED_MODE, SC_LEDC_CHANNEL_FLOOR_HEATING_PUMP, floor_heating_pump_pwm));
ESP_ERROR_CHECK(ledc_update_duty(LEDC_HIGH_SPEED_MODE, SC_LEDC_CHANNEL_PANEL_PUMP));
ESP_ERROR_CHECK(ledc_update_duty(LEDC_HIGH_SPEED_MODE, SC_LEDC_CHANNEL_FLOOR_HEATING_PUMP));
// Only publish data when connected to the MQTT broker and time is synchronized with NTP
if (IS_READY(MQTT_CONNECTED_BIT|TIME_SYNC_BIT)) {
mqtt_publish_data("solar_panel_temperature", (json_value){MQTT_TYPE_FLOAT, {.f = readings[panel]}});
mqtt_publish_data("floor_temperature", (json_value){MQTT_TYPE_FLOAT, {.f = readings[floor_heating]}});
mqtt_publish_data("store_higher_temperature", (json_value){MQTT_TYPE_FLOAT, {.f = readings[store_high]}});
mqtt_publish_data("store_lower_temperature", (json_value){MQTT_TYPE_FLOAT, {.f = readings[store_low]}});
mqtt_publish_data("panel_pump_duty_cycle", (json_value){MQTT_TYPE_INT, {.i = panel_pump_pwm}});
mqtt_publish_data("floor_heating_pump_duty_cycle", (json_value){MQTT_TYPE_INT, {.i = floor_heating_pump_pwm}});
}
vTaskDelayUntil(&last_wake_time, (1000 * CONFIG_SC_SAMPLE_PERIOD) / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
static void solar_pwm_init() {
// Prepare and then apply the LEDC PWM timer configuration
ledc_timer_config_t ledc_timer_0 = {
.speed_mode = LEDC_HIGH_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = LEDC_TIMER_8_BIT,
.freq_hz = CONFIG_SC_PUMP_PWM_FREQ,
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer_0));
// Prepare and then apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel_0 = {
.speed_mode = LEDC_HIGH_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = CONFIG_SC_PANEL_PUMP_GPIO,
.duty = 0, // Set duty to 0%
.hpoint = 0
};
ledc_channel_config_t ledc_channel_1 = {
.speed_mode = LEDC_HIGH_SPEED_MODE,
.channel = LEDC_CHANNEL_1,
.timer_sel = LEDC_TIMER_0,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = CONFIG_SC_FLOOR_HEATING_PUMP_GPIO,
.duty = 0, // Set duty to 0%
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel_0));
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel_1));
}
static int sensor_index(char * address) {
for (int i = 0; i < SC_EXPECTED_TEMPERATURE_SENSORS_COUNT; i++) {
char rom_code_s[17];
owb_string_from_rom_code(temperature_sensors[i].rom_code, rom_code_s, sizeof(rom_code_s));
if (strcmp(address, rom_code_s) == 0) {
return i;
}
}
return -1;
}
static void solar_ds18b20_init() {
// Stable readings require a brief period before communication
ESP_LOGI(SOLAR_LOGGER, "Waiting before 1-wire enumeration...");
vTaskDelay(2000.0 / portTICK_PERIOD_MS);
// Create a 1-Wire bus, using the RMT timeslot driver
owb = owb_rmt_initialize(&rmt_driver_info, CONFIG_SC_1WIRE_GPIO, RMT_CHANNEL_1, RMT_CHANNEL_0);
owb_use_crc(owb, true); // enable CRC check for ROM code
// Find all connected temperature sensors
ESP_LOGI(SOLAR_LOGGER, "List of all DS18B20 devices on bus:");
OneWireBus_ROMCode device_rom_codes[SC_MAX_TEMPERATURE_SENSORS_COUNT] = {0};
int num_devices = 0;
OneWireBus_SearchState search_state = {0};
bool found = false;
owb_search_first(owb, &search_state, &found);
while (found) {
char rom_code_s[17];
owb_string_from_rom_code(search_state.rom_code, rom_code_s, sizeof(rom_code_s));
ESP_LOGI(SOLAR_LOGGER, "device %d: %s", num_devices, rom_code_s);
device_rom_codes[num_devices] = search_state.rom_code;
++num_devices;
owb_search_next(owb, &search_state, &found);
}
ESP_LOGI(SOLAR_LOGGER, "Found %d device%s", num_devices, num_devices == 1 ? "" : "s");
if (num_devices != SC_EXPECTED_TEMPERATURE_SENSORS_COUNT) {
ESP_LOGE(SOLAR_LOGGER, "Cannot find exactly %d temperature sensors, rebooting!", SC_EXPECTED_TEMPERATURE_SENSORS_COUNT);
vTaskDelay(5000.0 / portTICK_PERIOD_MS);
esp_restart();
}
// Initializes all temperature sensors
for (int i = 0; i < num_devices; ++i)
{
ds18b20_init(&temperature_sensors[i], owb, device_rom_codes[i]); // associate with bus and device
ds18b20_use_crc(&temperature_sensors[i], true); // enable CRC check on all reads
ds18b20_set_resolution(&temperature_sensors[i], DS18B20_RESOLUTION_12_BIT); // use max. resolution
}
// Extract sensor addresses from NVS
nvs_handle_t nvs;
ESP_ERROR_CHECK(nvs_open("solar", NVS_READONLY, &nvs));
char * panel_sensor_addr = get_nvs_string(nvs, "panel_sensor");
assert(panel_sensor_addr != NULL);
char * store_higher_sensor_addr = get_nvs_string(nvs, "store_h_sensor");
assert(store_higher_sensor_addr != NULL);
char * store_lower_sensor_addr = get_nvs_string(nvs, "store_l_sensor");
assert(store_lower_sensor_addr != NULL);
char * floor_heating_sensor_addr = get_nvs_string(nvs, "fl_ht_sensor");
assert(floor_heating_sensor_addr != NULL);
if (panel_sensor_addr == NULL || store_higher_sensor_addr == NULL || store_lower_sensor_addr == NULL || floor_heating_sensor_addr == NULL) {
ESP_LOGE(SOLAR_LOGGER, "Cannot find one of the temperature sensor addresses, check your configuration!");
vTaskDelay(5000.0 / portTICK_PERIOD_MS);
esp_restart();
}
// Match each sensor with its function
panel = sensor_index(panel_sensor_addr);
assert(panel != -1);
store_high = sensor_index(store_higher_sensor_addr);
assert(store_high != -1);
store_low = sensor_index(store_lower_sensor_addr);
assert(store_low != -1);
floor_heating = sensor_index(floor_heating_sensor_addr);
assert(floor_heating != -1);
if (panel == -1 || store_high == -1 || store_low == -1 || floor_heating == -1) {
ESP_LOGE(SOLAR_LOGGER, "Cannot find one of the temperature sensors, check your configuration!");
vTaskDelay(5000.0 / portTICK_PERIOD_MS);
esp_restart();
}
}
void solar_init() {
solar_pwm_init();
solar_ds18b20_init();
// Create a task to handle solar monitoring
BaseType_t xReturned;
xReturned = xTaskCreate(solar_process,
"solar_process",
CONFIG_SC_STACK_SIZE, /* Stack size in words, not bytes. */
NULL, /* Parameter passed into the task. */
tskIDLE_PRIORITY + 12,
NULL);
if (xReturned != pdPASS) {
ESP_LOGE(SOLAR_LOGGER, "xTaskCreate('solar_process'): %d", xReturned);
abort();
}
}