diff --git a/devices/sensor/bh1750_sensor.cpp b/devices/sensor/bh1750_sensor.cpp new file mode 100644 index 0000000..d66c01d --- /dev/null +++ b/devices/sensor/bh1750_sensor.cpp @@ -0,0 +1,131 @@ +#include "bh1750_sensor.h" + +Bh1750Sensor::Bh1750Sensor() { +} + +bool Bh1750Sensor::begin(const Bh1750Config &config) { + _config = config; + + Wire.begin(_config.sda_pin, _config.scl_pin); + + /* + Пробуем перевести датчик в выбранный режим измерения. + */ + if (!send_command(_config.measurement_mode)) { + _initialized = false; + _online = false; + return false; + } + + _initialized = true; + _online = true; + return true; +} + +void Bh1750Sensor::update() { + if (!_initialized) { + return; + } + + uint32_t now_ms = millis(); + + if (now_ms - _last_read_attempt_ms < _config.read_interval_ms) { + return; + } + + _last_read_attempt_ms = now_ms; + + uint16_t raw_value = 0; + if (!read_measurement(raw_value)) { + _online = false; + return; + } + + _online = true; + _has_valid_data = true; + _last_success_read_ms = now_ms; + _last_raw_value = raw_value; + + /* + Согласно даташиту: + lux = raw / 1.2 + */ + _raw_lux = static_cast(raw_value) / 1.2f; + + update_filtered_lux(_raw_lux); +} + +float Bh1750Sensor::get_raw_lux() const { + return _raw_lux; +} + +float Bh1750Sensor::get_filtered_lux() const { + return _filtered_lux; +} + +bool Bh1750Sensor::has_valid_data() const { + return _has_valid_data; +} + +bool Bh1750Sensor::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; +} + +bool Bh1750Sensor::is_online() const { + return _online; +} + +String Bh1750Sensor::get_state_json() const { + String json = "{"; + json += "\"sensor\":\"bh1750\","; + 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 += "\"raw_value\":" + String(_last_raw_value) + ","; + json += "\"raw_lux\":" + String(_raw_lux, 2) + ","; + json += "\"filtered_lux\":" + String(_filtered_lux, 2); + json += "}"; + + json += "}"; + + return json; +} + +bool Bh1750Sensor::send_command(uint8_t command) { + Wire.beginTransmission(_config.i2c_address); + Wire.write(command); + return (Wire.endTransmission() == 0); +} + +bool Bh1750Sensor::read_measurement(uint16_t &raw_out) { + Wire.requestFrom((int)_config.i2c_address, 2); + + if (Wire.available() != 2) { + return false; + } + + uint8_t msb = Wire.read(); + uint8_t lsb = Wire.read(); + + raw_out = (static_cast(msb) << 8) | lsb; + return true; +} + +void Bh1750Sensor::update_filtered_lux(float new_lux) { + if (!_has_valid_data || _filtered_lux == 0.0f) { + _filtered_lux = new_lux; + return; + } + + _filtered_lux = + (_filtered_lux * (1.0f - _config.lux_ema_alpha)) + + (new_lux * _config.lux_ema_alpha); +} \ No newline at end of file diff --git a/devices/sensor/bh1750_sensor.h b/devices/sensor/bh1750_sensor.h new file mode 100644 index 0000000..0b71704 --- /dev/null +++ b/devices/sensor/bh1750_sensor.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include + +/* + Конфигурация датчика освещённости BH1750. +*/ +struct Bh1750Config { + uint8_t sda_pin = 16; + uint8_t scl_pin = 17; + uint8_t i2c_address = 0x5C; + + /* + Режим измерения: + 0x10 = Continuous High Resolution Mode + Это самый удобный стартовый режим. + */ + uint8_t measurement_mode = 0x10; + + /* + Через сколько миллисекунд без успешного чтения считать данные устаревшими. + */ + uint32_t stale_after_ms = 2000; + + /* + Интервал между попытками чтения. + Для continuous mode слишком часто дёргать датчик не нужно. + */ + uint32_t read_interval_ms = 200; + + /* + EMA-фильтр для сглаживания. + 0.0 -> почти без обновления + 1.0 -> без сглаживания + */ + float lux_ema_alpha = 0.25f; +}; + +/* + Драйвер / обёртка для BH1750 без внешних библиотек. +*/ +class Bh1750Sensor { +public: + Bh1750Sensor(); + + /* + Инициализация I2C и перевод датчика в рабочий режим. + */ + bool begin(const Bh1750Config &config); + + /* + Периодическое обновление состояния датчика. + Нужно вызывать часто из loop(). + */ + void update(); + + /* + Последнее сырое значение lux без фильтра. + */ + float get_raw_lux() const; + + /* + Последнее сглаженное значение lux. + */ + float get_filtered_lux() const; + + /* + Есть ли валидные данные. + */ + bool has_valid_data() const; + + /* + Не устарели ли данные. + */ + bool is_stale() const; + + /* + Доступен ли датчик. + */ + bool is_online() const; + + /* + JSON состояния датчика. + */ + String get_state_json() const; + +private: + Bh1750Config _config; + + 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; + + uint16_t _last_raw_value = 0; + float _raw_lux = 0.0f; + float _filtered_lux = 0.0f; + +private: + bool send_command(uint8_t command); + bool read_measurement(uint16_t &raw_out); + void update_filtered_lux(float new_lux); +}; \ No newline at end of file diff --git a/devices/sensor/bme280_sensor.cpp b/devices/sensor/bme280_sensor.cpp new file mode 100644 index 0000000..c76d402 --- /dev/null +++ b/devices/sensor/bme280_sensor.cpp @@ -0,0 +1,308 @@ +#include "bme280_sensor.h" + +Bme280Sensor::Bme280Sensor() { +} + +Bme280Sensor::~Bme280Sensor() { + if (_bme280 != nullptr) { + delete _bme280; + _bme280 = nullptr; + } + + if (_bmp280 != nullptr) { + delete _bmp280; + _bmp280 = nullptr; + } +} + +bool Bme280Sensor::begin(TwoWire &wire, const Bme280Config &config) { + _config = config; + _wire = &wire; + + _wire -> begin(_config.sda_pin, _config.scl_pin, 400000); + + /* + На случай повторной инициализации. + */ + 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; + + _temperature_c = 0.0f; + _pressure_hpa = 0.0f; + _humidity_percent = 0.0f; + + _filtered_temperature_c = 0.0f; + _filtered_pressure_hpa = 0.0f; + _filtered_humidity_percent = 0.0f; + + /* + Сначала пробуем BME280. + */ + if (init_bme280()) { + _sensor_type = BME_SENSOR_TYPE_BME280; + _initialized = true; + _online = true; + return true; + } + + /* + Если не вышло, пробуем BMP280. + */ + if (init_bmp280()) { + _sensor_type = BME_SENSOR_TYPE_BMP280; + _initialized = true; + _online = true; + return true; + } + + _sensor_type = BME_SENSOR_TYPE_UNKNOWN; + _initialized = false; + _online = false; + return false; +} + +void Bme280Sensor::update() { + if (!_initialized) { + return; + } + + uint32_t now_ms = millis(); + + 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; + + if (_sensor_type == BME_SENSOR_TYPE_BME280) { + if (_bme280 == nullptr) { + _online = false; + return; + } + + 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; + _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; + + if (isnan(temperature) || isnan(pressure)) { + _online = false; + return; + } + + _temperature_c = temperature; + _pressure_hpa = pressure; + _humidity_percent = 0.0f; + + update_filtered_temperature(temperature); + update_filtered_pressure(pressure); + } + + _online = true; + _has_valid_data = true; + _last_success_read_ms = now_ms; +} + +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; +} + +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_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; +} + +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 += "\"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 += "}"; + return json; +} + +bool Bme280Sensor::init_bme280() { + if (_wire == nullptr) { + return false; + } + + _bme280 = new Adafruit_BME280(); + + if (_bme280 == nullptr) { + return false; + } + + bool ok = _bme280 -> begin(_config.i2c_address, _wire); + + if (!ok) { + delete _bme280; + _bme280 = nullptr; + return false; + } + + _bme280 -> setSampling( + Adafruit_BME280::MODE_NORMAL, + Adafruit_BME280::SAMPLING_X2, + Adafruit_BME280::SAMPLING_X16, + Adafruit_BME280::SAMPLING_X1, + Adafruit_BME280::FILTER_X4, + Adafruit_BME280::STANDBY_MS_500 + ); + + return true; +} + +bool Bme280Sensor::init_bmp280() { + if (_wire == nullptr) { + return false; + } + + _bmp280 = new Adafruit_BMP280(_wire); + + if (_bmp280 == nullptr) { + return false; + } + + bool ok = _bmp280 -> begin(_config.i2c_address); + + if (!ok) { + delete _bmp280; + _bmp280 = nullptr; + return false; + } + + _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; +} + +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); +} + +void Bme280Sensor::update_filtered_pressure(float value) { + if (!_has_valid_data || _filtered_pressure_hpa == 0.0f) { + _filtered_pressure_hpa = value; + return; + } + + _filtered_pressure_hpa = + (_filtered_pressure_hpa * (1.0f - _config.pressure_ema_alpha)) + + (value * _config.pressure_ema_alpha); +} + +void Bme280Sensor::update_filtered_humidity(float value) { + if (!_has_valid_data || _filtered_humidity_percent == 0.0f) { + _filtered_humidity_percent = value; + return; + } + + _filtered_humidity_percent = + (_filtered_humidity_percent * (1.0f - _config.humidity_ema_alpha)) + + (value * _config.humidity_ema_alpha); +} \ No newline at end of file diff --git a/devices/sensor/bme280_sensor.h b/devices/sensor/bme280_sensor.h new file mode 100644 index 0000000..b860583 --- /dev/null +++ b/devices/sensor/bme280_sensor.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include + +/* + Конфигурация датчика BME280 / BMP280. +*/ +struct Bme280Config { + uint8_t sda_pin = 18; + uint8_t scl_pin = 19; + uint8_t i2c_address = 0x76; + + /* + Через сколько миллисекунд без успешного чтения + данные считаются устаревшими. + */ + uint32_t stale_after_ms = 3000; + + /* + Интервал чтения датчика. + */ + uint32_t read_interval_ms = 1000; + + /* + Коэффициенты сглаживания EMA. + */ + float temperature_ema_alpha = 0.25f; + float pressure_ema_alpha = 0.20f; + float humidity_ema_alpha = 0.20f; +}; + +enum BmeSensorType { + BME_SENSOR_TYPE_UNKNOWN = 0, + BME_SENSOR_TYPE_BMP280, + BME_SENSOR_TYPE_BME280 +}; + +class Bme280Sensor { +public: + Bme280Sensor(); + ~Bme280Sensor(); + + /* + Инициализация собственной I2C-шины и попытка + подключения BME280/BMP280. + */ + bool begin(TwoWire &wire, const Bme280Config &config); + + /* + Периодическое обновление состояния датчика. + */ + void update(); + + /* + Статус датчика. + */ + bool is_online() const; + bool has_valid_data() const; + bool is_stale() const; + + /* + Тип найденного датчика. + */ + BmeSensorType get_sensor_type() const; + String get_sensor_type_string() const; + + /* + Текущие значения. + */ + float get_temperature_c() const; + float get_pressure_hpa() const; + float get_humidity_percent() const; + + /* + Есть ли влажность. + Для BMP280 всегда false. + */ + bool has_humidity() const; + + /* + JSON состояния. + */ + String get_state_json() const; + +private: + Bme280Config _config; + + 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; + + 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 _filtered_temperature_c = 0.0f; + float _filtered_pressure_hpa = 0.0f; + float _filtered_humidity_percent = 0.0f; + +private: + bool init_bme280(); + bool init_bmp280(); + + void update_filtered_temperature(float value); + void update_filtered_pressure(float value); + void update_filtered_humidity(float value); +}; \ No newline at end of file diff --git a/devices/sensor/sensor.ino b/devices/sensor/sensor.ino index 5ad1579..a9b4bb2 100644 --- a/devices/sensor/sensor.ino +++ b/devices/sensor/sensor.ino @@ -1,144 +1,209 @@ #include + #include "ld2420_radar.h" +#include "bh1750_sensor.h" +#include "bme280_sensor.h" /* - Главный файл примера использования первого сенсорного слоя LD2420. - - Что делает этот пример: - - поднимает Serial для отладки - - создаёт и настраивает драйвер радара - - регулярно вызывает radar.update() - - печатает JSON состояния в монитор порта - - Дальше этот же модуль можно будет встроить в твоё ядро умного дома, - где вместо Serial.println(json) будет: - - HTTP-ответ - - отправка на сервер - - публикация в MQTT - - включение в общий /status endpoint + ========================= + GLOBAL OBJECTS + ========================= */ Ld2420Radar radar; RadarConfig radar_config; +Bh1750Sensor light_sensor; +Bh1750Config light_config; + +Bme280Sensor climate_sensor; +Bme280Config climate_config; + /* - Интервал печати JSON в монитор порта. - Слишком часто печатать не стоит, чтобы не засорять лог. + Отдельные I2C-шины. + 0-я шина для BH1750. + 1-я шина для BME/BMP280. +*/ +TwoWire i2c_climate = TwoWire(1); + +/* + Интервал печати состояния. */ static const uint32_t print_interval_ms = 500; uint32_t last_print_ms = 0; /* - Отдельный интервал печати короткой калибровочной строки. - Пока не обязателен, но полезен, если включить calibration_mode. + Интервал печати калибровочной строки радара. */ static const uint32_t calibration_print_interval_ms = 250; uint32_t last_calibration_print_ms = 0; + +/* + ========================= + RADAR CONFIGURATION + ========================= +*/ + void setup_radar_config() { - /* - Под твою текущую схему подключения: - TX радара -> GPIO4 ESP (RX) - RX радара -> GPIO15 ESP (TX) - */ radar_config.uart_rx_pin = 4; radar_config.uart_tx_pin = 15; radar_config.baud_rate = 115200; - /* - Удержание присутствия после исчезновения сырых сообщений. - */ radar_config.presence_hold_ms = 1500; - - /* - Если давно не было данных от радара, сенсор помечается stale. - */ radar_config.stale_after_ms = 2000; - /* - Базовые фильтры. - */ radar_config.median_window_size = 5; radar_config.max_zone_step_per_sample = 4; radar_config.distance_ema_alpha = 0.35f; radar_config.speed_ema_alpha = 0.25f; radar_config.speed_epsilon_m_s = 0.08f; - /* - Границы валидных зон. - Пока задай широкий диапазон. Потом подрежешь под конкретный корпус, - геометрию комнаты и реальный рабочий сектор. - */ radar_config.min_valid_zone = 1; radar_config.max_valid_zone = 200; - /* - Коэффициент перевода зоны в метры. - Это пока стартовое приближение. - Позже откалибруешь по реальным измерениям. - */ radar_config.zone_to_meter_k = 0.7f; radar_config.zone_to_meter_b = 0.0f; - /* - Если понадобится отладка строк, которые не распарсились. - */ radar_config.enable_debug_unknown_lines = false; } + +/* + ========================= + BH1750 CONFIGURATION + ========================= +*/ + +void setup_light_config() { + /* + Отдельная шина освещённости: + SDA -> GPIO16 + SCL -> GPIO17 + ADDR -> VCC -> 0x5C + */ + light_config.sda_pin = 16; + light_config.scl_pin = 17; + light_config.i2c_address = 0x5C; + light_config.measurement_mode = 0x10; + light_config.stale_after_ms = 2000; + light_config.read_interval_ms = 200; + light_config.lux_ema_alpha = 0.25f; +} + + +/* + ========================= + BME/BMP280 CONFIGURATION + ========================= +*/ + +void setup_climate_config() { + /* + Отдельная климатическая шина: + SDA -> GPIO18 + SCL -> GPIO19 + SDO -> GND => 0x76 + SDO -> VCC => 0x77 + */ + climate_config.sda_pin = 18; + climate_config.scl_pin = 19; + climate_config.i2c_address = 0x76; + + climate_config.stale_after_ms = 3000; + climate_config.read_interval_ms = 1000; + + climate_config.temperature_ema_alpha = 0.25f; + climate_config.pressure_ema_alpha = 0.20f; + climate_config.humidity_ema_alpha = 0.20f; +} + + +/* + ========================= + DEVICE INITIALIZATION + ========================= +*/ + +void init_radar() { + setup_radar_config(); + + radar.begin(Serial2, radar_config); + radar.set_calibration_mode(false); + + Serial.println("Radar initialized"); +} + +void init_light_sensor() { + setup_light_config(); + + bool ok = light_sensor.begin(light_config); + + if (ok) { + Serial.println("BH1750 initialized"); + } else { + Serial.println("BH1750 init failed"); + } +} + +void init_climate_sensor() { + setup_climate_config(); + + bool ok = climate_sensor.begin(i2c_climate, climate_config); + + if (ok) { + Serial.print("Climate sensor initialized: "); + Serial.println(climate_sensor.get_sensor_type_string()); + } else { + Serial.println("Climate sensor init failed"); + } +} + + +/* + ========================= + SETUP + ========================= +*/ + void setup() { Serial.begin(115200); delay(1000); Serial.println(); - Serial.println("ld2420 radar project start"); + Serial.println("Radar + BH1750 + BME/BMP280 project start"); - setup_radar_config(); - - radar.begin(Serial2, radar_config); - - /* - Если нужен компактный лог для ручной калибровки, - можно включить так: - radar.set_calibration_mode(true); - - Пока оставим выключенным, чтобы печатать JSON. - */ - radar.set_calibration_mode(false); - - Serial.println("radar initialized"); + init_radar(); + init_light_sensor(); + init_climate_sensor(); } + +/* + ========================= + LOOP + ========================= +*/ + void loop() { radar.update(); + light_sensor.update(); + climate_sensor.update(); uint32_t now_ms = millis(); - /* - Обычный режим: печатаем JSON состояния. - Это как раз тот формат, который потом будет удобно отдать наружу - в твою систему умного дома. - */ if (!radar.is_calibration_mode()) { if (now_ms - last_print_ms >= print_interval_ms) { last_print_ms = now_ms; - String json = radar.get_state_json(); - Serial.println(json); + Serial.println(radar.get_state_json()); + Serial.println(light_sensor.get_state_json()); + Serial.println(climate_sensor.get_state_json()); } } else { - /* - Калибровочный режим: - печатаем короткую строку, чтобы можно было быстро ходить с рулеткой - и смотреть соответствие Range реальной дистанции. - */ if (now_ms - last_calibration_print_ms >= calibration_print_interval_ms) { last_calibration_print_ms = now_ms; Serial.println(radar.get_calibration_line()); } } - - /* - Здесь не делаем delay(), чтобы update() вызывался максимально часто. - */ } \ No newline at end of file