diff --git a/devices/sensor/bh1750_sensor.cpp b/devices/sensor/bh1750_sensor.cpp index d66c01d..87daeb0 100644 --- a/devices/sensor/bh1750_sensor.cpp +++ b/devices/sensor/bh1750_sensor.cpp @@ -6,6 +6,8 @@ bool Bh1750Sensor::begin(const Bh1750Config &config) { _config = config; + if (_config.lux_max <= 0.0f) _config.lux_max = 1000.0f; + Wire.begin(_config.sda_pin, _config.scl_pin); /* @@ -63,6 +65,26 @@ return _filtered_lux; } +uint8_t Bh1750Sensor::get_level() const { + if (!_has_valid_data) return 0; + + float normalized = _filtered_lux / _config.lux_max; + if (normalized < 0.0f) normalized = 0.0f; + if (normalized > 1.0f) normalized = 1.0f; + + return (uint8_t)(normalized * 10.0f + 0.5f); +} + +uint8_t Bh1750Sensor::get_percent() const { + if (!_has_valid_data) return 0; + + float normalized = _filtered_lux / _config.lux_max; + if (normalized < 0.0f) normalized = 0.0f; + if (normalized > 1.0f) normalized = 1.0f; + + return (uint8_t)(normalized * 100.0f + 0.5f); +} + bool Bh1750Sensor::has_valid_data() const { return _has_valid_data; } @@ -82,18 +104,10 @@ 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 += "\"online\":" + String(_online ? "true" : "false") + ","; + json += "\"level\":" + String(get_level()) + ","; + json += "\"lux\":" + String(_filtered_lux, 2) + ","; + json += "\"percent\":" + String(get_percent()); json += "}"; return json; diff --git a/devices/sensor/bh1750_sensor.h b/devices/sensor/bh1750_sensor.h index 0b71704..0c54eba 100644 --- a/devices/sensor/bh1750_sensor.h +++ b/devices/sensor/bh1750_sensor.h @@ -35,6 +35,14 @@ 1.0 -> без сглаживания */ float lux_ema_alpha = 0.25f; + + /* + Максимальное значение lux, соответствующее level=10 и percent=100. + BH1750 в High Resolution Mode выдаёт до ~65535 lux (прямой солнечный свет). + Для офиса / квартиры разумный потолок — 1000 lux. + Откалибруйте под условия эксплуатации. + */ + float lux_max = 1000.0f; }; /* @@ -66,6 +74,16 @@ float get_filtered_lux() const; /* + Уровень освещения 0..10 (относительно lux_max). + */ + uint8_t get_level() const; + + /* + Уровень освещения в процентах 0..100 (относительно lux_max). + */ + uint8_t get_percent() const; + + /* Есть ли валидные данные. */ bool has_valid_data() const; @@ -82,6 +100,13 @@ /* JSON состояния датчика. + Формат: + { + "online": true, + "level": 4, + "lux": 160.15, + "percent": 11 + } */ String get_state_json() const; diff --git a/devices/sensor/max4466_mic.cpp b/devices/sensor/max4466_mic.cpp new file mode 100644 index 0000000..cecb203 --- /dev/null +++ b/devices/sensor/max4466_mic.cpp @@ -0,0 +1,312 @@ +#include "max4466_mic.h" +#include + +/* + ========================================================= + MAX4466 Microphone — реализация + ========================================================= +*/ + +/* ==================================================== + BEGIN + ==================================================== */ + +bool Max4466Mic::begin(const Max4466Config &config) { + _config = config; + + /* Валидация и зажим параметров */ + if (_config.sample_count == 0) _config.sample_count = 256; + if (_config.db_avg_window_s < 1) _config.db_avg_window_s = 1; + if (_config.db_avg_window_s > DB_AVG_MAX) _config.db_avg_window_s = DB_AVG_MAX; + if (_config.db_trend_window_min < 2) _config.db_trend_window_min = 2; + if (_config.db_trend_window_min > DB_TREND_MAX) _config.db_trend_window_min = DB_TREND_MAX; + if (_config.db_ref_mv <= 0.0f) _config.db_ref_mv = 1.0f; + + analogReadResolution(12); + pinMode(_config.adc_pin, INPUT); + + _last_peak_reset_ms = millis(); + + /* + Аналоговый датчик не подтверждает наличие — + считаем инициализацию всегда успешной. + */ + _online = true; + return true; +} + + +/* ==================================================== + UPDATE + ==================================================== */ + +void Max4466Mic::update() { + uint32_t now_ms = millis(); + + if (now_ms - _last_read_ms < _config.read_interval_ms) { + return; + } + + _last_read_ms = now_ms; + + /* Снимаем RMS */ + float rms_mv = _measure_rms_mv(); + + /* + Если RMS слишком мал — скорее всего пин не подключён + или питание отсутствует. Но мы не уходим в offline — + просто применяем noise floor. + */ + _rms_mv = rms_mv; + + float db = _rms_to_db(rms_mv); + _db_raw = db; + + _update_filtered_db(db); + + /* Пик */ + if (db > _db_peak) { + _db_peak = db; + } + + /* Сброс пика по таймеру */ + if (_config.peak_reset_ms > 0 && + (now_ms - _last_peak_reset_ms) >= _config.peak_reset_ms) { + _db_peak = db; + _last_peak_reset_ms = now_ms; + } + + _has_valid_data = true; + _last_success_read_ms = now_ms; + + _tick_db_avg(now_ms); + _tick_trend(now_ms); +} + + +/* ==================================================== + СТАТУС + ==================================================== */ + +bool Max4466Mic::is_stale() const { + if (!_has_valid_data) return true; + return (millis() - _last_success_read_ms) > _config.stale_after_ms; +} + +void Max4466Mic::reset_peak() { + _db_peak = _db_filtered; + _last_peak_reset_ms = millis(); +} + + +/* ==================================================== + ЗАМЕР RMS + ==================================================== */ + +float Max4466Mic::_measure_rms_mv() { + /* + Алгоритм: + 1. Снять N отсчётов + 2. Вычислить DC-смещение (среднее) + 3. Вычислить дисперсию (RMS переменной составляющей) + + MAX4466 имеет выход с DC-смещением ~VCC/2 (≈ 1650 мВ при 3.3В). + Нас интересует только переменная составляющая — амплитуда звука. + */ + + uint32_t sum = 0; + uint16_t n = _config.sample_count; + + /* Буфер на стеке — не больше 512 отсчётов во избежание переполнения */ + if (n > 512) n = 512; + + uint16_t buf[512]; + + for (uint16_t i = 0; i < n; i++) { + buf[i] = (uint16_t)analogRead(_config.adc_pin); + sum += buf[i]; + if (_config.sample_interval_us > 0) { + delayMicroseconds(_config.sample_interval_us); + } + } + + /* DC offset */ + float dc = (float)sum / (float)n; + + /* RMS переменной составляющей */ + float sum_sq = 0.0f; + for (uint16_t i = 0; i < n; i++) { + float diff = (float)buf[i] - dc; + sum_sq += diff * diff; + } + + float rms_lsb = sqrtf(sum_sq / (float)n); + + /* Перевод LSB → мВ */ + float mv_per_lsb = _config.adc_vref_mv / (float)_config.adc_max_value; + return rms_lsb * mv_per_lsb; +} + + +/* ==================================================== + RMS → dB + ==================================================== */ + +float Max4466Mic::_rms_to_db(float rms_mv) const { + /* + dB = 20 * log10(rms_mv / ref_mv) + ref_mv = db_ref_mv из конфига. + + Если rms_mv ниже noise floor в абсолютных единицах — + возвращаем db_noise_floor, чтобы не было -inf и артефактов. + */ + if (rms_mv < 1e-6f) { + return _config.db_noise_floor; + } + + float db = 20.0f * log10f(rms_mv / _config.db_ref_mv); + + if (db < _config.db_noise_floor) { + db = _config.db_noise_floor; + } + + return db; +} + + +/* ==================================================== + EMA-ФИЛЬТР dB + ==================================================== */ + +void Max4466Mic::_update_filtered_db(float db) { + if (!_has_valid_data) { + _db_filtered = db; + return; + } + + _db_filtered = + (_db_filtered * (1.0f - _config.db_ema_alpha)) + + (db * _config.db_ema_alpha); +} + + +/* ==================================================== + СЕКУНДНОЕ УСРЕДНЕНИЕ + ==================================================== */ + +void Max4466Mic::_tick_db_avg(uint32_t now_ms) { + if (_db_avg_last_ms == 0) { _db_avg_last_ms = now_ms; return; } + if (now_ms - _db_avg_last_ms < 1000) return; + _db_avg_last_ms = now_ms; + + uint8_t win = _config.db_avg_window_s; + _db_avg_buf[_db_avg_idx] = _db_filtered; + _db_avg_idx = (_db_avg_idx + 1) % win; + if (_db_avg_count < win) _db_avg_count++; + + float sum = 0.0f; + for (uint8_t i = 0; i < _db_avg_count; i++) sum += _db_avg_buf[i]; + _db_avg = sum / (float)_db_avg_count; +} + + +/* ==================================================== + ТРЕНД ПО МИНУТАМ + ==================================================== */ + +void Max4466Mic::_tick_trend(uint32_t now_ms) { + if (_trend_last_ms == 0) { _trend_last_ms = now_ms; return; } + if (now_ms - _trend_last_ms < 60000u) return; + _trend_last_ms = now_ms; + + uint8_t win = _config.db_trend_window_min; + _trend_buf[_trend_idx] = _db_avg; + _trend_idx = (_trend_idx + 1) % win; + if (_trend_count < win) _trend_count++; + + _compute_trend(); +} + +void Max4466Mic::_compute_trend() { + uint8_t n = _trend_count; + if (n < 2) { _trend_slope = 0; _trend_std_dev = 0.0f; return; } + + uint8_t win = _config.db_trend_window_min; + float sx = 0, sy = 0, sxx = 0, sxy = 0, mean_y = 0; + + for (uint8_t i = 0; i < n; i++) { + uint8_t idx = (uint8_t)((_trend_idx - n + i + win) % win); + float x = (float)i, y = _trend_buf[idx]; + 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-6f) { + _trend_slope = 0; + } else { + /* + Порог наклона: 0.5 dB/мин. + Изменение меньше этого считается «постоянным». + */ + float slope = (fn * sxy - sx * sy) / denom; + _trend_slope = (slope > 0.5f) ? 1 : (slope < -0.5f) ? -1 : 0; + } + + mean_y /= fn; + float var = 0.0f; + for (uint8_t i = 0; i < n; i++) { + uint8_t idx = (uint8_t)((_trend_idx - n + i + win) % win); + float d = _trend_buf[idx] - mean_y; + var += d * d; + } + _trend_std_dev = sqrtf(var / fn); +} + +const char* Max4466Mic::get_noise_dynamics() const { + if (_trend_count < 2) return "constant"; + if (_trend_std_dev > 3.0f && _trend_slope == 0) return "variable"; + if (_trend_slope > 0) return "increasing"; + if (_trend_slope < 0) return "decreasing"; + return "constant"; +} + + +/* ==================================================== + NOISE LEVEL 0..10 + ==================================================== */ + +uint8_t Max4466Mic::get_noise_level() const { + if (!_has_valid_data) return 0; + + float range = _config.db_scale_range_db; + if (range < 1.0f) range = 1.0f; + + float normalized = (_db_avg - _config.db_noise_floor) / range; + + if (normalized < 0.0f) normalized = 0.0f; + if (normalized > 1.0f) normalized = 1.0f; + + return (uint8_t)(normalized * 10.0f + 0.5f); +} + + +/* ==================================================== + JSON + ==================================================== */ + +String Max4466Mic::get_state_json() const { + String json = "{"; + json += "\"online\":" + String(_online ? "true" : "false") + ","; + json += "\"current_noise\":" + String((int)(_db_filtered + 0.5f)) + ","; + json += "\"noise_level\":" + String(get_noise_level()) + ","; + json += "\"noise_level_dbi\":" + String((int)(_db_avg + 0.5f)) + ","; + json += "\"noise_dynamics\":\"" + String(get_noise_dynamics()) + "\""; + json += "}"; + return json; +} \ No newline at end of file diff --git a/devices/sensor/max4466_mic.h b/devices/sensor/max4466_mic.h new file mode 100644 index 0000000..6d3a1d1 --- /dev/null +++ b/devices/sensor/max4466_mic.h @@ -0,0 +1,247 @@ +#pragma once + +#include + +/* + ========================================================= + MAX4466 Microphone — драйвер для ESP32 + Подключение: аналоговый выход на ADC-пин ESP32 + + Что умеет: + - Замер мгновенного уровня шума (dB SPL, приблизительно) + - EMA-сглаживание dB + - Динамика шума: скользящее среднее по минутам + + линейный тренд (как у LD2420) + - Пиковый уровень за окно наблюдения + - JSON состояния + + Калибровка: + Абсолютные dB SPL зависят от усиления потенциометра + на модуле MAX4466 и акустики помещения. + Настройте db_ref_mv и db_ref_pa под ваши условия, + либо используйте относительные значения dB FS. + + Как работает замер: + За каждый вызов update() снимается sample_count отсчётов + с интервалом sample_interval_us. + Вычисляется RMS амплитуды (после вычитания DC-смещения), + затем переводится в дБ. + ========================================================= +*/ + +struct Max4466Config { + /* ---- Пин и ADC ---- */ + uint8_t adc_pin = 34; + + /* + Разрешение ADC ESP32: 12 бит → max = 4095. + Напряжение питания 3.3 В → 1 LSB ≈ 0.806 мВ. + */ + uint16_t adc_max_value = 4095; + float adc_vref_mv = 3300.0f; // мВ + + /* ---- Сэмплирование ---- */ + + /* + Количество отсчётов за одно измерение. + Больше → точнее RMS, но дольше блокируется loop(). + 128–256 — хороший баланс для замера шума. + */ + uint16_t sample_count = 256; + + /* + Пауза между отсчётами в микросекундах. + 256 отсчётов × 100 мкс = ~25 мс на одно измерение. + */ + uint16_t sample_interval_us = 100; + + /* ---- Интервал между измерениями ---- */ + uint32_t read_interval_ms = 100; + + /* + Через сколько мс без успешного чтения считать данные устаревшими. + */ + uint32_t stale_after_ms = 2000; + + /* ---- EMA-сглаживание dB ---- */ + /* + 0.0 → почти без обновления (сильное сглаживание) + 1.0 → без сглаживания + */ + float db_ema_alpha = 0.15f; + + /* ---- Калибровка dB ---- */ + + /* + Опорное напряжение в мВ соответствующее 0 dB SPL (≈ 20 мкПа). + Подберите экспериментально или рассчитайте по даташиту + на MAX4466 при заданном усилении. + По умолчанию: 1 мВ RMS → 0 dB FS (относительные dB). + Если нужен абсолютный dB SPL — откалибруйте под усиление. + */ + float db_ref_mv = 1.0f; + + /* + Нижний порог шума (dB): значения ниже считаются тишиной. + Компенсирует шум АЦП при полной тишине. + */ + float db_noise_floor = 30.0f; + + /* + Диапазон шкалы noise_level (0..10). + noise_level = 0 соответствует db_noise_floor, + noise_level = 10 соответствует db_noise_floor + db_scale_range_db. + По умолчанию: 30 dB диапазон → каждый балл ≈ 3 dB. + */ + float db_scale_range_db = 30.0f; + + /* ---- Динамика (тренд по минутам) ---- */ + + /* + Усреднение по секундам для activity_score (0–10). + Аналогично activity_avg_window_s у LD2420. + */ + uint8_t db_avg_window_s = 30; + + /* + Окно тренда в минутах. + Минимум 2, максимум DB_TREND_MAX. + */ + uint8_t db_trend_window_min = 10; + + /* ---- Пиковое значение ---- */ + + /* + Период сброса пикового значения (мс). + 0 = никогда не сбрасывать автоматически. + */ + uint32_t peak_reset_ms = 60000; +}; + + +/* + ========================================================= + Max4466Mic — публичный интерфейс + ========================================================= +*/ + +class Max4466Mic { +public: + Max4466Mic() = default; + + /* + Инициализация: настройка ADC-пина. + Возвращает true всегда (аналоговый датчик не отвечает + на запросы — online определяем по наличию данных). + */ + bool begin(const Max4466Config &config); + + /* + Периодическое обновление. + Нужно вызывать часто из loop(). + */ + void update(); + + /* ---- Статус ---- */ + bool is_online() const { return _online; } + bool has_valid_data() const { return _has_valid_data; } + bool is_stale() const; + + /* ---- Текущие значения ---- */ + + /* + Мгновенный dB (сглаженный EMA). + */ + float get_db() const { return _db_filtered; } + + /* + Сырой dB без EMA-фильтра. + */ + float get_db_raw() const { return _db_raw; } + + /* + Усреднённый dB за db_avg_window_s секунд. + */ + float get_db_avg() const { return _db_avg; } + + /* + Пиковый dB за период peak_reset_ms. + */ + float get_db_peak() const { return _db_peak; } + + /* + RMS-напряжение последнего замера (мВ). + */ + float get_rms_mv() const { return _rms_mv; } + + /* ---- Динамика ---- */ + + /* + Тренд уровня шума по минутам. + Возвращает: "increasing" / "decreasing" / "constant" / "variable" + */ + const char* get_noise_dynamics() const; + + /* + Средний уровень шума за минуту в виде шкалы 0..10. + Вычисляется из db_avg относительно db_noise_floor и + db_noise_floor + db_scale_range_db. + */ + uint8_t get_noise_level() const; + + /* ---- JSON ---- */ + /* + Формат: + { + "online": true, + "current_noise": 65, // dB (мгновенный, EMA-сглаженный) + "noise_level": 4, // 0..10 (среднее за минуту) + "noise_level_dbi": 65, // dB (среднее за минуту) + "noise_dynamics": "constant" + } + */ + String get_state_json() const; + + /* ---- Ручной сброс пика ---- */ + void reset_peak(); + +private: + Max4466Config _config; + + bool _online = false; + bool _has_valid_data = false; + + uint32_t _last_read_ms = 0; + uint32_t _last_success_read_ms = 0; + uint32_t _last_peak_reset_ms = 0; + + float _rms_mv = 0.0f; + float _db_raw = 0.0f; + float _db_filtered = 0.0f; + float _db_avg = 0.0f; + float _db_peak = 0.0f; + + /* ---- Внутренние методы ---- */ + float _measure_rms_mv(); + float _rms_to_db(float rms_mv) const; + void _update_filtered_db(float db); + + /* ---- Секундное усреднение (как act_avg у LD2420) ---- */ + static constexpr uint8_t DB_AVG_MAX = 60; + float _db_avg_buf[DB_AVG_MAX] = {}; + uint8_t _db_avg_idx = 0; + uint8_t _db_avg_count = 0; + uint32_t _db_avg_last_ms = 0; + void _tick_db_avg(uint32_t now_ms); + + /* ---- Тренд по минутам (как _tick_trend у LD2420) ---- */ + static constexpr uint8_t DB_TREND_MAX = 10; + float _trend_buf[DB_TREND_MAX] = {}; + uint8_t _trend_idx = 0; + uint8_t _trend_count = 0; + uint32_t _trend_last_ms = 0; + int8_t _trend_slope = 0; + float _trend_std_dev = 0.0f; + void _tick_trend(uint32_t now_ms); + void _compute_trend(); +}; \ No newline at end of file diff --git a/devices/sensor/sensor.ino b/devices/sensor/sensor.ino index 246291e..5b23cbc 100644 --- a/devices/sensor/sensor.ino +++ b/devices/sensor/sensor.ino @@ -3,6 +3,7 @@ #include "ld2420_radar.h" #include "bh1750_sensor.h" #include "bme280_sensor.h" +#include "max4466_mic.h" /* ========================= @@ -19,6 +20,9 @@ Bme280Sensor climate_sensor; Bme280Config climate_config; +Max4466Mic mic; +Max4466Config mic_config; + /* Отдельные I2C-шины. 0-я шина для BH1750. @@ -129,6 +133,7 @@ light_config.stale_after_ms = 2000; light_config.read_interval_ms = 200; light_config.lux_ema_alpha = 0.25f; + light_config.lux_max = 500.0f; } @@ -161,6 +166,50 @@ /* ========================= + MAX4466 CONFIGURATION + ========================= +*/ + +void setup_mic_config() { + /* + Аналоговый выход MAX4466 -> GPIO34 + GPIO34 — input only, ADC1_CH6, подходит для analogRead. + + Калибровка: + db_noise_floor — уровень тишины в вашем помещении (дБ). + db_scale_range_db — диапазон от тишины до максимума (дБ). + db_ref_mv — опорное напряжение для расчёта дБ. + + Стартовые значения подходят для большинства помещений. + Для точной калибровки: + 1. В тишине посмотрите db_raw в Serial — это ваш noise floor. + 2. При максимальном ожидаемом шуме посмотрите db_raw — + разница с noise floor и есть db_scale_range_db. + */ + mic_config.adc_pin = 34; + mic_config.adc_max_value = 4095; + mic_config.adc_vref_mv = 3300.0f; + + mic_config.sample_count = 256; + mic_config.sample_interval_us = 100; + + mic_config.read_interval_ms = 100; + mic_config.stale_after_ms = 2000; + + mic_config.db_ema_alpha = 0.15f; + mic_config.db_ref_mv = 1.0f; + mic_config.db_noise_floor = 30.0f; + mic_config.db_scale_range_db = 30.0f; + + mic_config.db_avg_window_s = 30; + mic_config.db_trend_window_min = 10; + + mic_config.peak_reset_ms = 60000; +} + + +/* + ========================= DEVICE INITIALIZATION ========================= */ @@ -174,18 +223,14 @@ 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()); @@ -194,6 +239,12 @@ } } +void init_mic() { + setup_mic_config(); + mic.begin(mic_config); + Serial.println("MAX4466 initialized"); +} + /* ========================= @@ -206,11 +257,12 @@ delay(1000); Serial.println(); - Serial.println("Radar + BH1750 + BME/BMP280 project start"); + Serial.println("Radar + BH1750 + BME/BMP280 + MAX4466 project start"); init_radar(); init_light_sensor(); init_climate_sensor(); + init_mic(); } @@ -224,6 +276,7 @@ radar.update(); light_sensor.update(); climate_sensor.update(); + mic.update(); uint32_t now_ms = millis(); @@ -233,5 +286,6 @@ Serial.println(radar.get_state_json()); Serial.println(light_sensor.get_state_json()); Serial.println(climate_sensor.get_state_json()); + Serial.println(mic.get_state_json()); } }