462 lines
15 KiB
C
462 lines
15 KiB
C
/**
|
|
* @file inputs.c
|
|
* @brief Implementation of input handling module.
|
|
*/
|
|
|
|
#include "inputs.h"
|
|
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "driver/gpio.h"
|
|
#include "esp_log.h"
|
|
#include <ds18x20.h>
|
|
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
/** @brief Maximum number of DS18B20 sensors supported. */
|
|
#define MAX_DN18B20_SENSORS 4U
|
|
|
|
/** @brief Number of retry attempts for 1-Wire read. */
|
|
#define ONE_WIRE_LOOPS 4U
|
|
|
|
/** @brief Task interval in seconds. */
|
|
#define PERIODIC_INTERVAL 1U
|
|
|
|
static const char *TAG = "inputs";
|
|
|
|
/** @brief Burner fault GPIO pin (from Kconfig). */
|
|
static const uint8_t uBurnerFaultPin = CONFIG_GPIO_BURNER_FAULT;
|
|
|
|
/** @brief DS18B20 1-Wire GPIO pin (from Kconfig). */
|
|
static const uint8_t uDS18B20Pin = CONFIG_GPIO_DS18B20_ONEWIRE;
|
|
|
|
/** @brief Chamber temperature sensor address (from Kconfig). */
|
|
static const onewire_addr_t uChamperTempSensorAddr = CONFIG_ONEWIRE_ADDR_CHAMBER_TEMP;
|
|
|
|
/** @brief Outdoor temperature sensor address (from Kconfig). */
|
|
static const onewire_addr_t uOutdoorTempSensorAddr = CONFIG_ONEWIRE_ADDR_OUTDOOR_TEMP;
|
|
|
|
/** @brief Inlet flow temperature sensor address (from Kconfig). */
|
|
static const onewire_addr_t uInletFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_INLET_FLOW_TEMP;
|
|
|
|
/** @brief Return flow temperature sensor address (from Kconfig). */
|
|
static const onewire_addr_t uReturnFlowTempSensorAddr = CONFIG_ONEWIRE_ADDR_RETURN_FLOW_TEMP;
|
|
|
|
static onewire_addr_t uOneWireAddresses[MAX_DN18B20_SENSORS];
|
|
static float fDS18B20Temps[MAX_DN18B20_SENSORS];
|
|
static size_t sSensorCount = 0U;
|
|
|
|
static SemaphoreHandle_t xMutexAccessInputs = NULL;
|
|
static eBurnerErrorState sBurnerErrorState;
|
|
static sMeasurement sChamperTemperature;
|
|
static sMeasurement sOutdoorTemperature;
|
|
static sMeasurement sInletFlowTemperature;
|
|
static sMeasurement sReturnFlowTemperature;
|
|
|
|
/* Private function prototypes */
|
|
static void taskInput(void *pvParameters);
|
|
static void initMeasurement(sMeasurement *pMeasurement);
|
|
static void updateAverage(sMeasurement *pMeasurement);
|
|
static void updatePrediction(sMeasurement *pMeasurement);
|
|
static float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex);
|
|
|
|
esp_err_t initInputs(void)
|
|
{
|
|
gpio_config_t ioConfBurnerFault = {
|
|
.pin_bit_mask = (1ULL << uBurnerFaultPin),
|
|
.mode = GPIO_MODE_INPUT,
|
|
.pull_up_en = GPIO_PULLUP_ENABLE,
|
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
|
.intr_type = GPIO_INTR_DISABLE};
|
|
|
|
esp_err_t ret = gpio_config(&ioConfBurnerFault);
|
|
if (ret != ESP_OK)
|
|
{
|
|
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret));
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
xMutexAccessInputs = xSemaphoreCreateRecursiveMutex();
|
|
if (xMutexAccessInputs == NULL)
|
|
{
|
|
ESP_LOGE(TAG, "Failed to create mutex");
|
|
return ESP_FAIL;
|
|
}
|
|
xSemaphoreGiveRecursive(xMutexAccessInputs);
|
|
|
|
initMeasurement(&sChamperTemperature);
|
|
initMeasurement(&sOutdoorTemperature);
|
|
initMeasurement(&sInletFlowTemperature);
|
|
initMeasurement(&sReturnFlowTemperature);
|
|
|
|
BaseType_t taskCreated = xTaskCreate(
|
|
taskInput,
|
|
"taskInput",
|
|
4096,
|
|
NULL,
|
|
5,
|
|
NULL);
|
|
|
|
if (taskCreated != pdPASS)
|
|
{
|
|
ESP_LOGE(TAG, "Failed to create task");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
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)
|
|
return;
|
|
|
|
pMeasurement->state = MEASUREMENT_FAULT;
|
|
pMeasurement->fCurrentValue = INITIALISATION_VALUE;
|
|
pMeasurement->fDampedValue = INITIALISATION_VALUE;
|
|
|
|
pMeasurement->average10s.fValue = INITIALISATION_VALUE;
|
|
pMeasurement->average10s.bufferCount = 0U;
|
|
pMeasurement->average10s.bufferIndex = 0U;
|
|
memset(pMeasurement->average10s.samples, 0U, sizeof(float) * AVG10S_SAMPLE_SIZE);
|
|
|
|
pMeasurement->average60s.fValue = INITIALISATION_VALUE;
|
|
pMeasurement->average60s.bufferCount = 0U;
|
|
pMeasurement->average60s.bufferIndex = 0U;
|
|
memset(pMeasurement->average60s.samples, 0U, sizeof(float) * AVG60S_SAMPLE_SIZE);
|
|
|
|
pMeasurement->predict60s.fValue = INITIALISATION_VALUE;
|
|
pMeasurement->predict60s.bufferCount = 0U;
|
|
pMeasurement->predict60s.bufferIndex = 0U;
|
|
memset(pMeasurement->predict60s.samples, 0U, sizeof(float) * PRED60S_SAMPLE_SIZE);
|
|
}
|
|
|
|
/**
|
|
* @brief Update average values and damped value for a measurement.
|
|
* @param pMeasurement Pointer to measurement structure.
|
|
*/
|
|
static void updateAverage(sMeasurement *pMeasurement)
|
|
{
|
|
if (!pMeasurement)
|
|
return;
|
|
|
|
/* 10-second average */
|
|
pMeasurement->average10s.samples[pMeasurement->average10s.bufferIndex] = pMeasurement->fCurrentValue;
|
|
pMeasurement->average10s.bufferIndex = (pMeasurement->average10s.bufferIndex + 1) % AVG10S_SAMPLE_SIZE;
|
|
|
|
if (pMeasurement->average10s.bufferCount < AVG10S_SAMPLE_SIZE)
|
|
{
|
|
pMeasurement->average10s.bufferCount++;
|
|
}
|
|
|
|
float sum = 0.0;
|
|
for (int i = 0; i < pMeasurement->average10s.bufferCount; i++)
|
|
{
|
|
sum += pMeasurement->average10s.samples[i];
|
|
}
|
|
|
|
if (pMeasurement->average10s.bufferCount == 0U)
|
|
{
|
|
pMeasurement->average10s.fValue = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
pMeasurement->average10s.fValue = sum / pMeasurement->average10s.bufferCount;
|
|
}
|
|
|
|
/* 60-second average */
|
|
pMeasurement->average60s.samples[pMeasurement->average60s.bufferIndex] = pMeasurement->fCurrentValue;
|
|
pMeasurement->average60s.bufferIndex = (pMeasurement->average60s.bufferIndex + 1) % AVG60S_SAMPLE_SIZE;
|
|
|
|
if (pMeasurement->average60s.bufferCount < AVG60S_SAMPLE_SIZE)
|
|
{
|
|
pMeasurement->average60s.bufferCount++;
|
|
}
|
|
|
|
sum = 0.0;
|
|
for (int i = 0; i <= pMeasurement->average60s.bufferCount; i++)
|
|
{
|
|
sum += pMeasurement->average60s.samples[i];
|
|
}
|
|
|
|
if (pMeasurement->average60s.bufferCount == 0U)
|
|
{
|
|
pMeasurement->average60s.fValue = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Update 60-second prediction using linear regression.
|
|
* @param pMeasurement Pointer to measurement structure.
|
|
*/
|
|
static void updatePrediction(sMeasurement *pMeasurement)
|
|
{
|
|
if (!pMeasurement)
|
|
return;
|
|
|
|
// Update predict60s buffer
|
|
sPredict *predict60s = &pMeasurement->predict60s;
|
|
predict60s->samples[predict60s->bufferIndex] = pMeasurement->fCurrentValue;
|
|
predict60s->bufferIndex = (predict60s->bufferIndex + 1) % PRED60S_SAMPLE_SIZE;
|
|
if (predict60s->bufferCount < PRED60S_SAMPLE_SIZE)
|
|
predict60s->bufferCount++;
|
|
|
|
// Predict 60s future value using linear regression
|
|
predict60s->fValue = linearRegressionPredict(
|
|
predict60s->samples,
|
|
predict60s->bufferCount,
|
|
predict60s->bufferIndex,
|
|
predict60s->bufferCount + 60.0f);
|
|
}
|
|
|
|
/**
|
|
* @brief Input task - reads sensors periodically.
|
|
* @param pvParameters Task parameters (unused).
|
|
*/
|
|
static void taskInput(void *pvParameters)
|
|
{
|
|
while (1)
|
|
{
|
|
vTaskDelay(PERIODIC_INTERVAL * 1000U / portTICK_PERIOD_MS);
|
|
if (xSemaphoreTakeRecursive(xMutexAccessInputs, portMAX_DELAY) == pdTRUE)
|
|
{
|
|
sChamperTemperature.state = MEASUREMENT_FAULT;
|
|
sOutdoorTemperature.state = MEASUREMENT_FAULT;
|
|
sInletFlowTemperature.state = MEASUREMENT_FAULT;
|
|
sReturnFlowTemperature.state = MEASUREMENT_FAULT;
|
|
|
|
if (gpio_get_level(uBurnerFaultPin) == 1)
|
|
{
|
|
sBurnerErrorState = FAULT;
|
|
}
|
|
else
|
|
{
|
|
sBurnerErrorState = NO_ERROR;
|
|
}
|
|
|
|
if (ds18x20_scan_devices(uDS18B20Pin, uOneWireAddresses, MAX_DN18B20_SENSORS, &sSensorCount) != ESP_OK)
|
|
{
|
|
ESP_LOGE(TAG, "1-Wire device scan error!");
|
|
}
|
|
|
|
if (!sSensorCount)
|
|
{
|
|
ESP_LOGW(TAG, "No 1-Wire devices detected!");
|
|
}
|
|
else
|
|
{
|
|
// ESP_LOGI(TAG, "%d 1-Wire devices detected", sSensorCount);
|
|
|
|
if (sSensorCount > MAX_DN18B20_SENSORS)
|
|
{
|
|
sSensorCount = MAX_DN18B20_SENSORS;
|
|
ESP_LOGW(TAG, "More 1-Wire devices found than expected!");
|
|
}
|
|
|
|
for (size_t iReadLoop = 0; iReadLoop < ONE_WIRE_LOOPS; iReadLoop++)
|
|
{
|
|
if (ds18x20_measure_and_read_multi(uDS18B20Pin, uOneWireAddresses, sSensorCount, fDS18B20Temps) != ESP_OK)
|
|
{
|
|
ESP_LOGE(TAG, "1-Wire devices read error");
|
|
vTaskDelay(PERIODIC_INTERVAL * 100U / portTICK_PERIOD_MS); // Wait 100ms if bus error occurred
|
|
}
|
|
else
|
|
{
|
|
for (int j = 0; j < sSensorCount; j++)
|
|
{
|
|
float temp_c = fDS18B20Temps[j];
|
|
// ESP_LOGI(TAG, "Sensor: %08" PRIx64 " reports %lf°C", (uint64_t)uOneWireAddresses[j], temp_c);
|
|
|
|
switch ((uint64_t)uOneWireAddresses[j])
|
|
{
|
|
case ((uint64_t)uChamperTempSensorAddr):
|
|
sChamperTemperature.fCurrentValue = temp_c;
|
|
sChamperTemperature.state = MEASUREMENT_NO_ERROR;
|
|
updateAverage(&sChamperTemperature);
|
|
updatePrediction(&sChamperTemperature);
|
|
break;
|
|
case ((uint64_t)uOutdoorTempSensorAddr):
|
|
sOutdoorTemperature.fCurrentValue = temp_c;
|
|
sOutdoorTemperature.state = MEASUREMENT_NO_ERROR;
|
|
updateAverage(&sOutdoorTemperature);
|
|
updatePrediction(&sOutdoorTemperature);
|
|
break;
|
|
case ((uint64_t)uInletFlowTempSensorAddr):
|
|
sInletFlowTemperature.fCurrentValue = temp_c;
|
|
sInletFlowTemperature.state = MEASUREMENT_NO_ERROR;
|
|
updateAverage(&sInletFlowTemperature);
|
|
updatePrediction(&sInletFlowTemperature);
|
|
break;
|
|
case ((uint64_t)uReturnFlowTempSensorAddr):
|
|
sReturnFlowTemperature.fCurrentValue = temp_c;
|
|
sReturnFlowTemperature.state = MEASUREMENT_NO_ERROR;
|
|
updateAverage(&sReturnFlowTemperature);
|
|
updatePrediction(&sReturnFlowTemperature);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
xSemaphoreGiveRecursive(xMutexAccessInputs);
|
|
}
|
|
else
|
|
{
|
|
sChamperTemperature.state = MEASUREMENT_FAULT;
|
|
sOutdoorTemperature.state = MEASUREMENT_FAULT;
|
|
sInletFlowTemperature.state = MEASUREMENT_FAULT;
|
|
sReturnFlowTemperature.state = MEASUREMENT_FAULT;
|
|
|
|
ESP_LOGE(TAG, "Unable to take mutex: taskInput()");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Predict future value using linear regression.
|
|
* @param samples Sample buffer.
|
|
* @param count Number of valid samples.
|
|
* @param bufferIndex Current buffer write index.
|
|
* @param futureIndex Future time index to predict.
|
|
* @return Predicted value.
|
|
*/
|
|
static float linearRegressionPredict(const float *samples, size_t count, size_t bufferIndex, float futureIndex)
|
|
{
|
|
if (count == 0)
|
|
return INITIALISATION_VALUE;
|
|
|
|
float sumX = INITIALISATION_VALUE, sumY = INITIALISATION_VALUE, sumXY = INITIALISATION_VALUE, sumX2 = INITIALISATION_VALUE;
|
|
|
|
for (size_t i = 0; i < count; i++)
|
|
{
|
|
size_t circularIndex = (bufferIndex + i + 1) % count;
|
|
|
|
float x = (float)i;
|
|
float y = samples[circularIndex];
|
|
|
|
sumX += x;
|
|
sumY += y;
|
|
sumXY += x * y;
|
|
sumX2 += x * x;
|
|
}
|
|
|
|
float denominator = (count * sumX2 - sumX * sumX);
|
|
if (fabs(denominator) < 1e-6)
|
|
return samples[bufferIndex];
|
|
|
|
float m = (count * sumXY - sumX * sumY) / denominator;
|
|
float b = (sumY - m * sumX) / count;
|
|
|
|
return m * futureIndex + b;
|
|
}
|
|
|
|
sMeasurement getChamberTemperature(void)
|
|
{
|
|
sMeasurement ret;
|
|
ret.state = MEASUREMENT_FAULT;
|
|
if (xSemaphoreTakeRecursive(xMutexAccessInputs, pdMS_TO_TICKS(5000)) == pdTRUE)
|
|
{
|
|
ret = sChamperTemperature;
|
|
xSemaphoreGiveRecursive(xMutexAccessInputs);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "Unable to take mutex: getChamberTemperature()");
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
sMeasurement getOutdoorTemperature(void)
|
|
{
|
|
sMeasurement ret;
|
|
ret.state = MEASUREMENT_FAULT;
|
|
if (xSemaphoreTakeRecursive(xMutexAccessInputs, pdMS_TO_TICKS(5000)) == pdTRUE)
|
|
{
|
|
ret = sOutdoorTemperature;
|
|
xSemaphoreGiveRecursive(xMutexAccessInputs);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "Unable to take mutex: getOutdoorTemperature()");
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
sMeasurement getInletFlowTemperature(void)
|
|
{
|
|
sMeasurement ret;
|
|
ret.state = MEASUREMENT_FAULT;
|
|
if (xSemaphoreTakeRecursive(xMutexAccessInputs, pdMS_TO_TICKS(5000)) == pdTRUE)
|
|
{
|
|
ret = sInletFlowTemperature;
|
|
xSemaphoreGiveRecursive(xMutexAccessInputs);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "Unable to take mutex: getInletFlowTemperature()");
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
sMeasurement getReturnFlowTemperature(void)
|
|
{
|
|
sMeasurement ret;
|
|
ret.state = MEASUREMENT_FAULT;
|
|
if (xSemaphoreTakeRecursive(xMutexAccessInputs, pdMS_TO_TICKS(5000)) == pdTRUE)
|
|
{
|
|
ret = sReturnFlowTemperature;
|
|
xSemaphoreGiveRecursive(xMutexAccessInputs);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "Unable to take mutex: getReturnFlowTemperature()");
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
eBurnerErrorState getBurnerError(void)
|
|
{
|
|
eBurnerErrorState ret = FAULT;
|
|
if (xSemaphoreTakeRecursive(xMutexAccessInputs, pdMS_TO_TICKS(5000)) == pdTRUE)
|
|
{
|
|
ret = sBurnerErrorState;
|
|
xSemaphoreGiveRecursive(xMutexAccessInputs);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "Unable to take mutex: getBurnerError()");
|
|
}
|
|
return ret;
|
|
}
|