diff --git a/devices/sensor/bme280_sensor.cpp b/devices/sensor/bme280_sensor.cpp index c76d402..6d911d4 100644 --- a/devices/sensor/bme280_sensor.cpp +++ b/devices/sensor/bme280_sensor.cpp @@ -1,4 +1,5 @@ #include "bme280_sensor.h" +#include Bme280Sensor::Bme280Sensor() { } @@ -17,222 +18,248 @@ bool Bme280Sensor::begin(TwoWire &wire, const Bme280Config &config) { _config = config; - _wire = &wire; + _wire = &wire; - _wire -> begin(_config.sda_pin, _config.scl_pin, 400000); + /* Зажим параметров тренда */ + if (_config.temperature_trend_window_min < 2) _config.temperature_trend_window_min = 2; + if (_config.temperature_trend_window_min > TREND_MAX) _config.temperature_trend_window_min = TREND_MAX; + if (_config.pressure_trend_window_min < 2) _config.pressure_trend_window_min = 2; + if (_config.pressure_trend_window_min > TREND_MAX) _config.pressure_trend_window_min = TREND_MAX; + if (_config.humidity_trend_window_min < 2) _config.humidity_trend_window_min = 2; + if (_config.humidity_trend_window_min > TREND_MAX) _config.humidity_trend_window_min = TREND_MAX; - /* - На случай повторной инициализации. - */ - if (_bme280 != nullptr) { - delete _bme280; - _bme280 = nullptr; - } + _wire->begin(_config.sda_pin, _config.scl_pin, 400000); - if (_bmp280 != nullptr) { - delete _bmp280; - _bmp280 = nullptr; - } + /* На случай повторной инициализации */ + if (_bme280 != nullptr) { delete _bme280; _bme280 = nullptr; } + if (_bmp280 != nullptr) { delete _bmp280; _bmp280 = nullptr; } - _sensor_type = BME_SENSOR_TYPE_UNKNOWN; - _initialized = false; - _online = false; - _has_valid_data = false; - _last_read_attempt_ms = 0; - _last_success_read_ms = 0; + _sensor_type = BME_SENSOR_TYPE_UNKNOWN; + _initialized = false; + _online = false; + _has_valid_data = false; + _last_read_attempt_ms = 0; + _last_success_read_ms = 0; - _temperature_c = 0.0f; - _pressure_hpa = 0.0f; - _humidity_percent = 0.0f; + _temperature_c = 0.0f; + _pressure_hpa = 0.0f; + _humidity_percent = 0.0f; - _filtered_temperature_c = 0.0f; - _filtered_pressure_hpa = 0.0f; + _filtered_temperature_c = 0.0f; + _filtered_pressure_hpa = 0.0f; _filtered_humidity_percent = 0.0f; - /* - Сначала пробуем BME280. - */ + /* Сброс трендов */ + _temp_trend_count = 0; _temp_trend_idx = 0; _temp_trend_last_ms = 0; + _pres_trend_count = 0; _pres_trend_idx = 0; _pres_trend_last_ms = 0; + _humi_trend_count = 0; _humi_trend_idx = 0; _humi_trend_last_ms = 0; + _temp_trend_slope = 0.0f; _temp_trend_std_dev = 0.0f; _temp_trend_dir = 0; + _pres_trend_slope = 0.0f; _pres_trend_std_dev = 0.0f; _pres_trend_dir = 0; + _humi_trend_slope = 0.0f; _humi_trend_std_dev = 0.0f; _humi_trend_dir = 0; + + /* Сначала пробуем BME280 */ if (init_bme280()) { _sensor_type = BME_SENSOR_TYPE_BME280; _initialized = true; - _online = true; + _online = true; return true; } - /* - Если не вышло, пробуем BMP280. - */ + /* Если не вышло — пробуем BMP280 */ if (init_bmp280()) { _sensor_type = BME_SENSOR_TYPE_BMP280; _initialized = true; - _online = true; + _online = true; return true; } _sensor_type = BME_SENSOR_TYPE_UNKNOWN; - _initialized = false; - _online = false; return false; } +/* ==================================================== + UPDATE + ==================================================== */ + void Bme280Sensor::update() { - if (!_initialized) { - return; - } + if (!_initialized) return; uint32_t now_ms = millis(); - if (now_ms - _last_read_attempt_ms < _config.read_interval_ms) { - return; - } + if (now_ms - _last_read_attempt_ms < _config.read_interval_ms) return; _last_read_attempt_ms = now_ms; float temperature = 0.0f; - float pressure = 0.0f; - float humidity = 0.0f; + float pressure = 0.0f; + float humidity = 0.0f; if (_sensor_type == BME_SENSOR_TYPE_BME280) { - if (_bme280 == nullptr) { - _online = false; - return; - } + if (_bme280 == nullptr) { _online = false; return; } - temperature = _bme280 -> readTemperature(); - pressure = _bme280 -> readPressure() / 100.0f; - humidity = _bme280 -> readHumidity(); + temperature = _bme280->readTemperature(); + pressure = _bme280->readPressure() / 100.0f; + humidity = _bme280->readHumidity(); if (isnan(temperature) || isnan(pressure) || isnan(humidity)) { _online = false; return; } - _temperature_c = temperature; - _pressure_hpa = pressure; + _temperature_c = temperature; + _pressure_hpa = pressure; _humidity_percent = humidity; update_filtered_temperature(temperature); update_filtered_pressure(pressure); update_filtered_humidity(humidity); - } else if (_sensor_type == BME_SENSOR_TYPE_BMP280) { - if (_bmp280 == nullptr) { - _online = false; - return; - } - temperature = _bmp280 -> readTemperature(); - pressure = _bmp280 -> readPressure() / 100.0f; + } else if (_sensor_type == BME_SENSOR_TYPE_BMP280) { + if (_bmp280 == nullptr) { _online = false; return; } + + temperature = _bmp280->readTemperature(); + pressure = _bmp280->readPressure() / 100.0f; if (isnan(temperature) || isnan(pressure)) { _online = false; return; } - _temperature_c = temperature; - _pressure_hpa = pressure; + _temperature_c = temperature; + _pressure_hpa = pressure; _humidity_percent = 0.0f; update_filtered_temperature(temperature); update_filtered_pressure(pressure); } - _online = true; + _online = true; _has_valid_data = true; _last_success_read_ms = now_ms; + + /* Обновляем тренды */ + _tick_temp_trend(now_ms); + _tick_pres_trend(now_ms); + if (_sensor_type == BME_SENSOR_TYPE_BME280) { + _tick_humi_trend(now_ms); + } } -bool Bme280Sensor::is_online() const { - return _online; -} +/* ==================================================== + СТАТУС + ==================================================== */ -bool Bme280Sensor::has_valid_data() const { - return _has_valid_data; -} +bool Bme280Sensor::is_online() const { return _online; } +bool Bme280Sensor::has_valid_data() const { return _has_valid_data; } bool Bme280Sensor::is_stale() const { - if (!_has_valid_data) { - return true; - } - - uint32_t now_ms = millis(); - return (now_ms - _last_success_read_ms) > _config.stale_after_ms; + if (!_has_valid_data) return true; + return (millis() - _last_success_read_ms) > _config.stale_after_ms; } -BmeSensorType Bme280Sensor::get_sensor_type() const { - return _sensor_type; -} - -String Bme280Sensor::get_sensor_type_string() const { - if (_sensor_type == BME_SENSOR_TYPE_BME280) { - return "bme280"; - } - - if (_sensor_type == BME_SENSOR_TYPE_BMP280) { - return "bmp280"; - } - +BmeSensorType Bme280Sensor::get_sensor_type() const { return _sensor_type; } +String Bme280Sensor::get_sensor_type_string() const { + if (_sensor_type == BME_SENSOR_TYPE_BME280) return "bme280"; + if (_sensor_type == BME_SENSOR_TYPE_BMP280) return "bmp280"; return "unknown"; } -float Bme280Sensor::get_temperature_c() const { - return _filtered_temperature_c; +float Bme280Sensor::get_temperature_c() const { return _filtered_temperature_c; } +float Bme280Sensor::get_pressure_hpa() const { return _filtered_pressure_hpa; } +float Bme280Sensor::get_humidity_percent() const { return _filtered_humidity_percent; } +bool Bme280Sensor::has_humidity() const { return _sensor_type == BME_SENSOR_TYPE_BME280; } + +/* ==================================================== + ДИНАМИКА — публичные геттеры + ==================================================== */ + +const char* Bme280Sensor::get_temperature_dynamics() const { + return _dynamics_string(_temp_trend_dir, _temp_trend_std_dev, + _config.temperature_variable_std, _temp_trend_count); } -float Bme280Sensor::get_pressure_hpa() const { - return _filtered_pressure_hpa; +const char* Bme280Sensor::get_pressure_dynamics() const { + return _dynamics_string(_pres_trend_dir, _pres_trend_std_dev, + _config.pressure_variable_std, _pres_trend_count); } -float Bme280Sensor::get_humidity_percent() const { - return _filtered_humidity_percent; +const char* Bme280Sensor::get_humidity_dynamics() const { + return _dynamics_string(_humi_trend_dir, _humi_trend_std_dev, + _config.humidity_variable_std, _humi_trend_count); } -bool Bme280Sensor::has_humidity() const { - return _sensor_type == BME_SENSOR_TYPE_BME280; -} +float Bme280Sensor::get_temperature_dynamics_val() const { return _temp_trend_slope; } +float Bme280Sensor::get_pressure_dynamics_val() const { return _pres_trend_slope; } +float Bme280Sensor::get_humidity_dynamics_val() const { return _humi_trend_slope; } -String Bme280Sensor::get_state_json() const { +/* ==================================================== + JSON + ==================================================== */ + +String Bme280Sensor::get_temperature_json() const { String json = "{"; - json += "\"sensor\":\"" + get_sensor_type_string() + "\","; - json += "\"online\":" + String(_online ? "true" : "false") + ","; - json += "\"has_valid_data\":" + String(_has_valid_data ? "true" : "false") + ","; - json += "\"stale\":" + String(is_stale() ? "true" : "false") + ","; - json += "\"i2c_address\":\"0x" + String(_config.i2c_address, HEX) + "\","; - - json += "\"data\":{"; - json += "\"temperature_c\":" + String(_filtered_temperature_c, 2) + ","; - json += "\"pressure_hpa\":" + String(_filtered_pressure_hpa, 2); - - if (has_humidity()) { - json += ","; - json += "\"humidity_percent\":" + String(_filtered_humidity_percent, 2); - } - - json += "}"; - + json += "\"online\":" + String(_online ? "true" : "false") + ","; + json += "\"current\":" + String(_filtered_temperature_c, 2) + ","; + json += "\"dynamics\":\"" + String(get_temperature_dynamics()) + "\","; + json += "\"dynamics_val\":" + String(get_temperature_dynamics_val(), 2); json += "}"; return json; } -bool Bme280Sensor::init_bme280() { - if (_wire == nullptr) { - return false; +String Bme280Sensor::get_pressure_json() const { + String json = "{"; + json += "\"online\":" + String(_online ? "true" : "false") + ","; + json += "\"current\":" + String(_filtered_pressure_hpa, 2) + ","; + json += "\"dynamics\":\"" + String(get_pressure_dynamics()) + "\","; + json += "\"dynamics_val\":" + String(get_pressure_dynamics_val(), 2); + json += "}"; + return json; +} + +String Bme280Sensor::get_humidity_json() const { + String json = "{"; + json += "\"online\":" + String(_online ? "true" : "false") + ","; + json += "\"current\":" + String(_filtered_humidity_percent, 2) + ","; + json += "\"dynamics\":\"" + String(get_humidity_dynamics()) + "\","; + json += "\"dynamics_val\":" + String(get_humidity_dynamics_val(), 2); + json += "}"; + return json; +} + +String Bme280Sensor::get_state_json() const { + String json = "{"; + json += "\"sensor\":\"" + get_sensor_type_string() + "\","; + json += "\"online\":" + String(_online ? "true" : "false") + ","; + json += "\"has_valid_data\":" + String(_has_valid_data ? "true" : "false") + ","; + json += "\"stale\":" + String(is_stale() ? "true" : "false") + ","; + json += "\"i2c_address\":\"0x" + String(_config.i2c_address, HEX) + "\","; + json += "\"temperature\":" + get_temperature_json() + ","; + json += "\"pressure\":" + get_pressure_json(); + if (has_humidity()) { + json += ",\"humidity\":" + get_humidity_json(); } + json += "}"; + return json; +} + +/* ==================================================== + ИНИЦИАЛИЗАЦИЯ ЧИПОВ + ==================================================== */ + +bool Bme280Sensor::init_bme280() { + if (_wire == nullptr) return false; _bme280 = new Adafruit_BME280(); + if (_bme280 == nullptr) return false; - if (_bme280 == nullptr) { - return false; - } - - bool ok = _bme280 -> begin(_config.i2c_address, _wire); - + bool ok = _bme280->begin(_config.i2c_address, _wire); if (!ok) { delete _bme280; _bme280 = nullptr; return false; } - _bme280 -> setSampling( + _bme280->setSampling( Adafruit_BME280::MODE_NORMAL, Adafruit_BME280::SAMPLING_X2, Adafruit_BME280::SAMPLING_X16, @@ -240,49 +267,44 @@ Adafruit_BME280::FILTER_X4, Adafruit_BME280::STANDBY_MS_500 ); - return true; } bool Bme280Sensor::init_bmp280() { - if (_wire == nullptr) { - return false; - } + if (_wire == nullptr) return false; _bmp280 = new Adafruit_BMP280(_wire); + if (_bmp280 == nullptr) return false; - if (_bmp280 == nullptr) { - return false; - } - - bool ok = _bmp280 -> begin(_config.i2c_address); - + bool ok = _bmp280->begin(_config.i2c_address); if (!ok) { delete _bmp280; _bmp280 = nullptr; return false; } - _bmp280 -> setSampling( + _bmp280->setSampling( Adafruit_BMP280::MODE_NORMAL, Adafruit_BMP280::SAMPLING_X2, Adafruit_BMP280::SAMPLING_X16, Adafruit_BMP280::FILTER_X4, Adafruit_BMP280::STANDBY_MS_500 ); - return true; } +/* ==================================================== + EMA-ФИЛЬТРЫ + ==================================================== */ + void Bme280Sensor::update_filtered_temperature(float value) { if (!_has_valid_data || _filtered_temperature_c == 0.0f) { _filtered_temperature_c = value; return; } - _filtered_temperature_c = (_filtered_temperature_c * (1.0f - _config.temperature_ema_alpha)) + - (value * _config.temperature_ema_alpha); + (value * _config.temperature_ema_alpha); } void Bme280Sensor::update_filtered_pressure(float value) { @@ -290,10 +312,9 @@ _filtered_pressure_hpa = value; return; } - _filtered_pressure_hpa = (_filtered_pressure_hpa * (1.0f - _config.pressure_ema_alpha)) + - (value * _config.pressure_ema_alpha); + (value * _config.pressure_ema_alpha); } void Bme280Sensor::update_filtered_humidity(float value) { @@ -301,8 +322,132 @@ _filtered_humidity_percent = value; return; } - _filtered_humidity_percent = (_filtered_humidity_percent * (1.0f - _config.humidity_ema_alpha)) + - (value * _config.humidity_ema_alpha); + (value * _config.humidity_ema_alpha); +} + +/* ==================================================== + ТИКИ ТРЕНДА (каждую минуту) + ==================================================== */ + +void Bme280Sensor::_tick_temp_trend(uint32_t now_ms) { + if (_temp_trend_last_ms == 0) { _temp_trend_last_ms = now_ms; return; } + if (now_ms - _temp_trend_last_ms < 60000u) return; + _temp_trend_last_ms = now_ms; + + uint8_t win = _config.temperature_trend_window_min; + _temp_trend_buf[_temp_trend_idx] = _filtered_temperature_c; + _temp_trend_idx = (_temp_trend_idx + 1) % win; + if (_temp_trend_count < win) _temp_trend_count++; + + _compute_trend( + _temp_trend_buf, win, _temp_trend_count, _temp_trend_idx, + _config.temperature_trend_threshold, _config.temperature_variable_std, + _temp_trend_slope, _temp_trend_std_dev, _temp_trend_dir); +} + +void Bme280Sensor::_tick_pres_trend(uint32_t now_ms) { + if (_pres_trend_last_ms == 0) { _pres_trend_last_ms = now_ms; return; } + if (now_ms - _pres_trend_last_ms < 60000u) return; + _pres_trend_last_ms = now_ms; + + uint8_t win = _config.pressure_trend_window_min; + _pres_trend_buf[_pres_trend_idx] = _filtered_pressure_hpa; + _pres_trend_idx = (_pres_trend_idx + 1) % win; + if (_pres_trend_count < win) _pres_trend_count++; + + _compute_trend( + _pres_trend_buf, win, _pres_trend_count, _pres_trend_idx, + _config.pressure_trend_threshold, _config.pressure_variable_std, + _pres_trend_slope, _pres_trend_std_dev, _pres_trend_dir); +} + +void Bme280Sensor::_tick_humi_trend(uint32_t now_ms) { + if (_humi_trend_last_ms == 0) { _humi_trend_last_ms = now_ms; return; } + if (now_ms - _humi_trend_last_ms < 60000u) return; + _humi_trend_last_ms = now_ms; + + uint8_t win = _config.humidity_trend_window_min; + _humi_trend_buf[_humi_trend_idx] = _filtered_humidity_percent; + _humi_trend_idx = (_humi_trend_idx + 1) % win; + if (_humi_trend_count < win) _humi_trend_count++; + + _compute_trend( + _humi_trend_buf, win, _humi_trend_count, _humi_trend_idx, + _config.humidity_trend_threshold, _config.humidity_variable_std, + _humi_trend_slope, _humi_trend_std_dev, _humi_trend_dir); +} + +/* ==================================================== + ЛИНЕЙНАЯ РЕГРЕССИЯ (статическая) + ==================================================== */ + +void Bme280Sensor::_compute_trend( + const float* buf, uint8_t win, uint8_t count, uint8_t idx, + float threshold, float variable_std, + float &out_slope, float &out_std_dev, int8_t &out_dir) +{ + if (count < 2) { + out_slope = 0.0f; + out_std_dev = 0.0f; + out_dir = 0; + return; + } + + uint8_t n = count; + float sx = 0.0f, sy = 0.0f, sxx = 0.0f, sxy = 0.0f, mean_y = 0.0f; + + for (uint8_t i = 0; i < n; i++) { + /* + Элементы в кольцевом буфере идут от самого старого к новому. + idx указывает на следующую позицию записи, т.е. самый старый + элемент находится по смещению (idx - count + win) % win. + */ + uint8_t bi = (uint8_t)((idx - n + i + win) % win); + float x = (float)i; + float y = buf[bi]; + sx += x; + sy += y; + sxx += x * x; + sxy += x * y; + mean_y += y; + } + + float fn = (float)n; + float denom = fn * sxx - sx * sx; + + if (fabsf(denom) < 1e-9f) { + out_slope = 0.0f; + out_dir = 0; + } else { + float slope = (fn * sxy - sx * sy) / denom; + out_slope = slope; + out_dir = (slope > threshold) ? 1 : + (slope < -threshold) ? -1 : 0; + } + + /* Стандартное отклонение от среднего */ + mean_y /= fn; + float var = 0.0f; + for (uint8_t i = 0; i < n; i++) { + uint8_t bi = (uint8_t)((idx - n + i + win) % win); + float d = buf[bi] - mean_y; + var += d * d; + } + out_std_dev = sqrtf(var / fn); +} + +/* ==================================================== + СТРОКА ДИНАМИКИ (статическая) + ==================================================== */ + +const char* Bme280Sensor::_dynamics_string( + int8_t dir, float std_dev, float variable_std, uint8_t count) +{ + if (count < 2) return "constant"; + if (dir == 0 && std_dev > variable_std) return "variable"; + if (dir > 0) return "increasing"; + if (dir < 0) return "decreasing"; + return "constant"; } \ No newline at end of file diff --git a/devices/sensor/bme280_sensor.h b/devices/sensor/bme280_sensor.h index b860583..43d8ace 100644 --- a/devices/sensor/bme280_sensor.h +++ b/devices/sensor/bme280_sensor.h @@ -28,8 +28,37 @@ Коэффициенты сглаживания EMA. */ float temperature_ema_alpha = 0.25f; - float pressure_ema_alpha = 0.20f; - float humidity_ema_alpha = 0.20f; + float pressure_ema_alpha = 0.20f; + float humidity_ema_alpha = 0.20f; + + /* + Окно тренда в минутах для каждого параметра. + Минимум 2, максимум TREND_MAX (10). + Тренд вычисляется по линейной регрессии скользящего + окна минутных снимков. + */ + uint8_t temperature_trend_window_min = 10; + uint8_t pressure_trend_window_min = 10; + uint8_t humidity_trend_window_min = 10; + + /* + Порог наклона линейной регрессии для классификации динамики. + Значения изменения меньше порога считаются "constant". + temperature: °C/мин + pressure: hPa/мин + humidity: %/мин + */ + float temperature_trend_threshold = 0.05f; // °C/мин + float pressure_trend_threshold = 0.10f; // hPa/мин + float humidity_trend_threshold = 0.10f; // %/мин + + /* + Порог стандартного отклонения для классификации "variable". + Если slope ≈ 0, но разброс велик — динамика непостоянная. + */ + float temperature_variable_std = 0.3f; // °C + float pressure_variable_std = 0.5f; // hPa + float humidity_variable_std = 1.0f; // % }; enum BmeSensorType { @@ -57,22 +86,22 @@ /* Статус датчика. */ - bool is_online() const; - bool has_valid_data() const; - bool is_stale() const; + bool is_online() const; + bool has_valid_data() const; + bool is_stale() const; /* Тип найденного датчика. */ - BmeSensorType get_sensor_type() const; - String get_sensor_type_string() const; + BmeSensorType get_sensor_type() const; + String get_sensor_type_string() const; /* - Текущие значения. + Текущие значения (EMA-сглаженные). */ - float get_temperature_c() const; - float get_pressure_hpa() const; - float get_humidity_percent() const; + float get_temperature_c() const; + float get_pressure_hpa() const; + float get_humidity_percent() const; /* Есть ли влажность. @@ -81,40 +110,121 @@ bool has_humidity() const; /* - JSON состояния. + Динамика параметра: + "constant" | "increasing" | "decreasing" | "variable" + */ + const char* get_temperature_dynamics() const; + const char* get_pressure_dynamics() const; + const char* get_humidity_dynamics() const; + + /* + Среднее изменение параметра в минуту за период тренда + (наклон линейной регрессии, единицы параметра/мин). + */ + float get_temperature_dynamics_val() const; + float get_pressure_dynamics_val() const; + float get_humidity_dynamics_val() const; + + /* + Три отдельных JSON-объекта состояния. + */ + String get_temperature_json() const; + String get_pressure_json() const; + String get_humidity_json() const; + + /* + Единый JSON (обратная совместимость). */ String get_state_json() const; private: Bme280Config _config; - TwoWire *_wire = nullptr; - - Adafruit_BME280 *_bme280 = nullptr; - Adafruit_BMP280 *_bmp280 = nullptr; + TwoWire *_wire = nullptr; + Adafruit_BME280 *_bme280 = nullptr; + Adafruit_BMP280 *_bmp280 = nullptr; BmeSensorType _sensor_type = BME_SENSOR_TYPE_UNKNOWN; - bool _initialized = false; - bool _online = false; - bool _has_valid_data = false; + bool _initialized = false; + bool _online = false; + bool _has_valid_data = false; uint32_t _last_read_attempt_ms = 0; uint32_t _last_success_read_ms = 0; - float _temperature_c = 0.0f; - float _pressure_hpa = 0.0f; - float _humidity_percent = 0.0f; + float _temperature_c = 0.0f; + float _pressure_hpa = 0.0f; + float _humidity_percent = 0.0f; - float _filtered_temperature_c = 0.0f; - float _filtered_pressure_hpa = 0.0f; - float _filtered_humidity_percent = 0.0f; + float _filtered_temperature_c = 0.0f; + float _filtered_pressure_hpa = 0.0f; + float _filtered_humidity_percent = 0.0f; private: + /* ---- Инициализация чипов ---- */ bool init_bme280(); bool init_bmp280(); + /* ---- EMA-фильтры ---- */ void update_filtered_temperature(float value); void update_filtered_pressure(float value); void update_filtered_humidity(float value); + + /* ==================================================== + ТРЕНД (минутный, линейная регрессия) + По аналогии с Ld2420Radar / Max4466Mic. + ==================================================== */ + + static constexpr uint8_t TREND_MAX = 10; + + /* -- температура -- */ + float _temp_trend_buf[TREND_MAX] = {}; + uint8_t _temp_trend_idx = 0; + uint8_t _temp_trend_count = 0; + uint32_t _temp_trend_last_ms = 0; + float _temp_trend_slope = 0.0f; // °C/мин (сырой наклон) + float _temp_trend_std_dev = 0.0f; + int8_t _temp_trend_dir = 0; // -1 / 0 / +1 + + /* -- давление -- */ + float _pres_trend_buf[TREND_MAX] = {}; + uint8_t _pres_trend_idx = 0; + uint8_t _pres_trend_count = 0; + uint32_t _pres_trend_last_ms = 0; + float _pres_trend_slope = 0.0f; + float _pres_trend_std_dev = 0.0f; + int8_t _pres_trend_dir = 0; + + /* -- влажность -- */ + float _humi_trend_buf[TREND_MAX] = {}; + uint8_t _humi_trend_idx = 0; + uint8_t _humi_trend_count = 0; + uint32_t _humi_trend_last_ms = 0; + float _humi_trend_slope = 0.0f; + float _humi_trend_std_dev = 0.0f; + int8_t _humi_trend_dir = 0; + + /* ---- Тики ---- */ + void _tick_temp_trend(uint32_t now_ms); + void _tick_pres_trend(uint32_t now_ms); + void _tick_humi_trend(uint32_t now_ms); + + /* + Общая функция вычисления линейной регрессии. + buf/win/count/idx — кольцевой буфер. + out_slope — наклон (единицы/мин). + out_std_dev — стандартное отклонение остатков. + out_dir — знак: -1 / 0 / +1 (с учётом порога threshold). + */ + static void _compute_trend( + const float* buf, uint8_t win, uint8_t count, uint8_t idx, + float threshold, float variable_std, + float &out_slope, float &out_std_dev, int8_t &out_dir); + + /* + Перевод dir + std в строку динамики. + */ + static const char* _dynamics_string( + int8_t dir, float std_dev, float variable_std, uint8_t count); }; \ No newline at end of file