Newer
Older
smart-home-server / devices / sensor / max4466_mic.cpp
#include "max4466_mic.h"
#include <math.h>

/*
    =========================================================
    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;
}